torch_em.data.datasets.medical.ct_lymph_nodes

The CT Lymph Nodes dataset contains annotations for lymph node segmentation in mediastinal and abdominal CT.

It consists of 176 CT volumes (90 mediastinal and 86 abdominal scans) with manually traced lymph node segmentations. The labels are instance labels, i.e. each lymph node has its own id. The CT scans are distributed as DICOM series (ca. 58 GB) and the segmentation masks as nifti files, which are stacked, aligned and stored together in hdf5 files by this module.

NOTE: This requires the pydicom python package.

The dataset is located at https://www.cancerimagingarchive.net/collection/ct-lymph-nodes/.

This dataset is from the publications https://doi.org/10.1007/978-3-319-10404-1_65 and https://doi.org/10.1007/978-3-319-24571-3_7 (segmentation masks). The data was released at https://doi.org/10.7937/K9/TCIA.2015.AQIIDCNM. Please cite it if you use this dataset in your research.

  1"""The CT Lymph Nodes dataset contains annotations for lymph node segmentation in mediastinal and abdominal CT.
  2
  3It consists of 176 CT volumes (90 mediastinal and 86 abdominal scans) with manually traced lymph node
  4segmentations. The labels are instance labels, i.e. each lymph node has its own id. The CT scans are distributed
  5as DICOM series (ca. 58 GB) and the segmentation masks as nifti files, which are stacked, aligned and stored
  6together in hdf5 files by this module.
  7
  8NOTE: This requires the pydicom python package.
  9
 10The dataset is located at https://www.cancerimagingarchive.net/collection/ct-lymph-nodes/.
 11
 12This dataset is from the publications https://doi.org/10.1007/978-3-319-10404-1_65 and
 13https://doi.org/10.1007/978-3-319-24571-3_7 (segmentation masks).
 14The data was released at https://doi.org/10.7937/K9/TCIA.2015.AQIIDCNM.
 15Please cite it if you use this dataset in your research.
 16"""
 17
 18import os
 19import csv
 20from glob import glob
 21from tqdm import tqdm
 22from natsort import natsorted
 23from typing import Union, Tuple, List, Optional, Literal
 24
 25import numpy as np
 26
 27from torch.utils.data import Dataset, DataLoader
 28
 29import torch_em
 30
 31from .. import util
 32
 33
 34URLS = {
 35    "images": "https://www.cancerimagingarchive.net/wp-content/uploads/TCIA_CT_Lymph_Nodes_06-22-2015.tcia",
 36    "labels": "https://www.cancerimagingarchive.net/wp-content/uploads/MED_ABD_LYMPH_MASKS.zip",
 37}
 38
 39CHECKSUMS = {
 40    "images": None,  # The DICOM series are downloaded individually from TCIA.
 41    "labels": "ace3475c21f04c3f3a01e7fa5181fcbcf4a98cc78ea945e058f1b001d25d6745",
 42}
 43
 44REGIONS = {"mediastinal": "MED", "abdominal": "ABD"}
 45
 46
 47def _load_dicom_volume(series_dir):
 48    """Stack a DICOM series into a volume with axes (z, y, x) and slices sorted by ascending patient z position.
 49
 50    Returns the volume in Hounsfield units and the image orientation (DICOM 'ImageOrientationPatient').
 51    """
 52    import pydicom
 53
 54    slices = [pydicom.dcmread(dcm_path) for dcm_path in natsorted(glob(os.path.join(series_dir, "*.dcm")))]
 55    slices.sort(key=lambda dcm: float(dcm.ImagePositionPatient[2]))
 56
 57    volume = np.stack([dcm.pixel_array for dcm in slices]).astype("float32")
 58    volume = volume * float(slices[0].RescaleSlope) + float(slices[0].RescaleIntercept)
 59    volume = np.round(volume).astype("int16")
 60
 61    orientation = np.round([float(v) for v in slices[0].ImageOrientationPatient]).astype("int").tolist()
 62    return volume, orientation
 63
 64
 65def _preprocess_ct_lymph_nodes(dicom_dir, label_dir, csv_path, preprocessed_dir):
 66    import h5py
 67    import nibabel as nib
 68
 69    with open(csv_path, "r") as f:
 70        subject_ids = {row["Subject ID"]: row["Series UID"] for row in csv.DictReader(f) if row["Modality"] == "CT"}
 71
 72    os.makedirs(preprocessed_dir, exist_ok=True)
 73    for subject_id, series_uid in tqdm(sorted(subject_ids.items()), desc="Preprocess CT Lymph Nodes"):
 74        out_path = os.path.join(preprocessed_dir, f"{subject_id}.h5")
 75        if os.path.exists(out_path):
 76            continue
 77
 78        volume, orientation = _load_dicom_volume(os.path.join(dicom_dir, series_uid))
 79        # The volume has axes (z, y, x) with x pointing to the patient's left and y to the posterior (DICOM LPS
 80        # convention with 'ImageOrientationPatient' [1, 0, 0, 0, 1, 0]). The labels are stored with the axis
 81        # orientation (L, P, S), so they only have to be transposed to match the volume.
 82        assert orientation == [1, 0, 0, 0, 1, 0], f"Unexpected image orientation for {subject_id}: {orientation}"
 83
 84        label_nifti = nib.load(os.path.join(label_dir, subject_id, f"{subject_id}_mask.nii.gz"))
 85        assert nib.aff2axcodes(label_nifti.affine) == ("L", "P", "S"), f"Unexpected label axes for {subject_id}"
 86        labels = np.asarray(label_nifti.dataobj).astype("uint8").transpose(2, 1, 0)
 87        assert labels.shape == volume.shape, f"Shape mismatch for {subject_id}: {labels.shape} vs {volume.shape}"
 88
 89        with h5py.File(out_path, "w") as f:
 90            f.create_dataset("raw", data=volume, compression="gzip")
 91            f.create_dataset("labels", data=labels, compression="gzip")
 92
 93
 94def get_ct_lymph_nodes_data(path: Union[os.PathLike, str], download: bool = False) -> str:
 95    """Download the CT Lymph Nodes dataset.
 96
 97    Args:
 98        path: Filepath to a folder where the data is downloaded for further processing.
 99        download: Whether to download the data if it is not present.
100
101    Returns:
102        Filepath where the preprocessed data is stored.
103    """
104    preprocessed_dir = os.path.join(path, "preprocessed")
105    if os.path.exists(preprocessed_dir):
106        return preprocessed_dir
107
108    os.makedirs(path, exist_ok=True)
109
110    # Download the labels.
111    label_dir = os.path.join(path, "MED_ABD_LYMPH_MASKS")
112    if not os.path.exists(label_dir):
113        zip_path = os.path.join(path, "MED_ABD_LYMPH_MASKS.zip")
114        util.download_source(path=zip_path, url=URLS["labels"], download=download, checksum=CHECKSUMS["labels"])
115        util.unzip(zip_path=zip_path, dst=path)
116
117    # Download the DICOM series from the TCIA manifest.
118    dicom_dir = os.path.join(path, "dicom")
119    csv_path = os.path.join(path, "ct_lymph_nodes_series")
120    util.download_source_tcia(
121        path=os.path.join(path, "TCIA_CT_Lymph_Nodes_06-22-2015.tcia"), url=URLS["images"], dst=dicom_dir,
122        csv_filename=csv_path, download=download,
123    )
124
125    _preprocess_ct_lymph_nodes(dicom_dir, label_dir, f"{csv_path}.csv", preprocessed_dir)
126    return preprocessed_dir
127
128
129def get_ct_lymph_nodes_paths(
130    path: Union[os.PathLike, str],
131    region: Optional[Literal["mediastinal", "abdominal"]] = None,
132    download: bool = False,
133) -> List[str]:
134    """Get paths to the CT Lymph Nodes data.
135
136    Args:
137        path: Filepath to a folder where the data is downloaded for further processing.
138        region: The choice of body region. Either 'mediastinal' or 'abdominal'. If None, all volumes are returned.
139        download: Whether to download the data if it is not present.
140
141    Returns:
142        List of filepaths for the hdf5 files, which contain the image data ('raw') and the label data ('labels').
143    """
144    data_dir = get_ct_lymph_nodes_data(path, download)
145
146    if region is None:
147        prefix = "*"
148    elif region in REGIONS:
149        prefix = REGIONS[region]
150    else:
151        raise ValueError(f"'{region}' is not a valid region. Please choose one of {list(REGIONS.keys())}.")
152
153    volume_paths = natsorted(glob(os.path.join(data_dir, f"{prefix}_LYMPH_*.h5")))
154    return volume_paths
155
156
157def get_ct_lymph_nodes_dataset(
158    path: Union[os.PathLike, str],
159    patch_shape: Tuple[int, ...],
160    region: Optional[Literal["mediastinal", "abdominal"]] = None,
161    resize_inputs: bool = False,
162    download: bool = False,
163    **kwargs
164) -> Dataset:
165    """Get the CT Lymph Nodes dataset for lymph node 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        region: The choice of body region. Either 'mediastinal' or 'abdominal'. If None, all volumes are returned.
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_ct_lymph_nodes_paths(path, region, 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_ct_lymph_nodes_loader(
198    path: Union[os.PathLike, str],
199    batch_size: int,
200    patch_shape: Tuple[int, ...],
201    region: Optional[Literal["mediastinal", "abdominal"]] = None,
202    resize_inputs: bool = False,
203    download: bool = False,
204    **kwargs
205) -> DataLoader:
206    """Get the CT Lymph Nodes dataloader for lymph node 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        region: The choice of body region. Either 'mediastinal' or 'abdominal'. If None, all volumes are returned.
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_ct_lymph_nodes_dataset(path, patch_shape, region, resize_inputs, download, **ds_kwargs)
222    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)
URLS = {'images': 'https://www.cancerimagingarchive.net/wp-content/uploads/TCIA_CT_Lymph_Nodes_06-22-2015.tcia', 'labels': 'https://www.cancerimagingarchive.net/wp-content/uploads/MED_ABD_LYMPH_MASKS.zip'}
CHECKSUMS = {'images': None, 'labels': 'ace3475c21f04c3f3a01e7fa5181fcbcf4a98cc78ea945e058f1b001d25d6745'}
REGIONS = {'mediastinal': 'MED', 'abdominal': 'ABD'}
def get_ct_lymph_nodes_data(path: Union[os.PathLike, str], download: bool = False) -> str:
 95def get_ct_lymph_nodes_data(path: Union[os.PathLike, str], download: bool = False) -> str:
 96    """Download the CT Lymph Nodes dataset.
 97
 98    Args:
 99        path: Filepath to a folder where the data is downloaded for further processing.
100        download: Whether to download the data if it is not present.
101
102    Returns:
103        Filepath where the preprocessed data is stored.
104    """
105    preprocessed_dir = os.path.join(path, "preprocessed")
106    if os.path.exists(preprocessed_dir):
107        return preprocessed_dir
108
109    os.makedirs(path, exist_ok=True)
110
111    # Download the labels.
112    label_dir = os.path.join(path, "MED_ABD_LYMPH_MASKS")
113    if not os.path.exists(label_dir):
114        zip_path = os.path.join(path, "MED_ABD_LYMPH_MASKS.zip")
115        util.download_source(path=zip_path, url=URLS["labels"], download=download, checksum=CHECKSUMS["labels"])
116        util.unzip(zip_path=zip_path, dst=path)
117
118    # Download the DICOM series from the TCIA manifest.
119    dicom_dir = os.path.join(path, "dicom")
120    csv_path = os.path.join(path, "ct_lymph_nodes_series")
121    util.download_source_tcia(
122        path=os.path.join(path, "TCIA_CT_Lymph_Nodes_06-22-2015.tcia"), url=URLS["images"], dst=dicom_dir,
123        csv_filename=csv_path, download=download,
124    )
125
126    _preprocess_ct_lymph_nodes(dicom_dir, label_dir, f"{csv_path}.csv", preprocessed_dir)
127    return preprocessed_dir

Download the CT Lymph Nodes 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 preprocessed data is stored.

def get_ct_lymph_nodes_paths( path: Union[os.PathLike, str], region: Optional[Literal['mediastinal', 'abdominal']] = None, download: bool = False) -> List[str]:
130def get_ct_lymph_nodes_paths(
131    path: Union[os.PathLike, str],
132    region: Optional[Literal["mediastinal", "abdominal"]] = None,
133    download: bool = False,
134) -> List[str]:
135    """Get paths to the CT Lymph Nodes data.
136
137    Args:
138        path: Filepath to a folder where the data is downloaded for further processing.
139        region: The choice of body region. Either 'mediastinal' or 'abdominal'. If None, all volumes are returned.
140        download: Whether to download the data if it is not present.
141
142    Returns:
143        List of filepaths for the hdf5 files, which contain the image data ('raw') and the label data ('labels').
144    """
145    data_dir = get_ct_lymph_nodes_data(path, download)
146
147    if region is None:
148        prefix = "*"
149    elif region in REGIONS:
150        prefix = REGIONS[region]
151    else:
152        raise ValueError(f"'{region}' is not a valid region. Please choose one of {list(REGIONS.keys())}.")
153
154    volume_paths = natsorted(glob(os.path.join(data_dir, f"{prefix}_LYMPH_*.h5")))
155    return volume_paths

Get paths to the CT Lymph Nodes data.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • region: The choice of body region. Either 'mediastinal' or 'abdominal'. If None, all volumes are returned.
  • 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_ct_lymph_nodes_dataset( path: Union[os.PathLike, str], patch_shape: Tuple[int, ...], region: Optional[Literal['mediastinal', 'abdominal']] = None, resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataset.Dataset:
158def get_ct_lymph_nodes_dataset(
159    path: Union[os.PathLike, str],
160    patch_shape: Tuple[int, ...],
161    region: Optional[Literal["mediastinal", "abdominal"]] = None,
162    resize_inputs: bool = False,
163    download: bool = False,
164    **kwargs
165) -> Dataset:
166    """Get the CT Lymph Nodes dataset for lymph node 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        region: The choice of body region. Either 'mediastinal' or 'abdominal'. If None, all volumes are returned.
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_ct_lymph_nodes_paths(path, region, 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 CT Lymph Nodes dataset for lymph node segmentation.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • patch_shape: The patch shape to use for training.
  • region: The choice of body region. Either 'mediastinal' or 'abdominal'. If None, 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_ct_lymph_nodes_loader( path: Union[os.PathLike, str], batch_size: int, patch_shape: Tuple[int, ...], region: Optional[Literal['mediastinal', 'abdominal']] = None, resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataloader.DataLoader:
198def get_ct_lymph_nodes_loader(
199    path: Union[os.PathLike, str],
200    batch_size: int,
201    patch_shape: Tuple[int, ...],
202    region: Optional[Literal["mediastinal", "abdominal"]] = None,
203    resize_inputs: bool = False,
204    download: bool = False,
205    **kwargs
206) -> DataLoader:
207    """Get the CT Lymph Nodes dataloader for lymph node 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        region: The choice of body region. Either 'mediastinal' or 'abdominal'. If None, all volumes are returned.
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_ct_lymph_nodes_dataset(path, patch_shape, region, resize_inputs, download, **ds_kwargs)
223    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)

Get the CT Lymph Nodes dataloader for lymph node 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.
  • region: The choice of body region. Either 'mediastinal' or 'abdominal'. If None, 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.