torch_em.data.datasets.medical.ctspine1k

The CTSpine1K dataset contains annotations for 25 vertebrae in CT scans.

The dataset consists of 1005 CT scans that were collected from four public collections and annotated with the individual vertebrae for this release. The 'source' argument selects one of them: 'colonog' (CT colonography), 'covid19', 'hnscc' (head and neck) and 'msd_liver' (the liver task of the Medical Segmentation Decathlon). The label ids are 1 to 7 for the cervical vertebrae C1 to C7, 8 to 19 for the thoracic vertebrae T1 to T12 and 20 to 25 for the lumbar vertebrae L1 to L6. See also CLASS_IDS.

NOTE: A scan only covers a part of the spine, so each one contains a contiguous range of the label ids rather than all 25.

NOTE: The scans of the CT colonography collection are named after the id of their DICOM series, whose dots hide the nifti extension from the file readers, so the data is linked under a prepared name.

The dataset is located at https://huggingface.co/datasets/alexanderdann/CTSpine1K and is distributed under the CC BY-NC-SA license. This dataset is from the publication https://doi.org/10.48550/arXiv.2105.14711. Please cite it if you use this dataset in your research.

  1"""The CTSpine1K dataset contains annotations for 25 vertebrae in CT scans.
  2
  3The dataset consists of 1005 CT scans that were collected from four public collections and annotated with
  4the individual vertebrae for this release. The 'source' argument selects one of them: 'colonog' (CT
  5colonography), 'covid19', 'hnscc' (head and neck) and 'msd_liver' (the liver task of the Medical
  6Segmentation Decathlon). The label ids are 1 to 7 for the cervical vertebrae C1 to C7, 8 to 19 for the
  7thoracic vertebrae T1 to T12 and 20 to 25 for the lumbar vertebrae L1 to L6. See also `CLASS_IDS`.
  8
  9NOTE: A scan only covers a part of the spine, so each one contains a contiguous range of the label ids
 10rather than all 25.
 11
 12NOTE: The scans of the CT colonography collection are named after the id of their DICOM series, whose
 13dots hide the nifti extension from the file readers, so the data is linked under a prepared name.
 14
 15The dataset is located at https://huggingface.co/datasets/alexanderdann/CTSpine1K and is distributed
 16under the CC BY-NC-SA license.
 17This dataset is from the publication https://doi.org/10.48550/arXiv.2105.14711.
 18Please cite it if you use this dataset in your research.
 19"""
 20
 21import os
 22from glob import glob
 23from natsort import natsorted
 24from typing import Union, Optional, Tuple, Literal, List
 25
 26from torch.utils.data import Dataset, DataLoader
 27
 28import torch_em
 29
 30from .. import util
 31
 32
 33REPO_ID = "alexanderdann/CTSpine1K"
 34
 35SOURCES = {
 36    "colonog": "COLONOG",
 37    "covid19": "COVID-19",
 38    "hnscc": "HNSCC-3DCT-RT",
 39    "msd_liver": "MSD-T10",
 40}
 41"""Mapping from the source choice to its folder in the release."""
 42
 43CLASS_NAMES = (
 44    [f"c{i}" for i in range(1, 8)] + [f"t{i}" for i in range(1, 13)] + [f"l{i}" for i in range(1, 7)]
 45)
 46"""The vertebrae of the CTSpine1K dataset. The label id of a vertebra is its 1-based index."""
 47
 48CLASS_IDS = {name: i + 1 for i, name in enumerate(CLASS_NAMES)}
 49"""Mapping from the vertebra name to its label id."""
 50
 51
 52def _prepare_name(path, prepared_dir, folder, stem):
 53    """Link a scan under a name without a dot in its stem, which the file readers need to find its extension."""
 54    out_path = os.path.join(prepared_dir, folder, f"{stem.replace('.', '_')}.nii.gz")
 55    if not os.path.exists(out_path):
 56        os.symlink(os.path.abspath(path), out_path)
 57    return out_path
 58
 59
 60def get_ctspine1k_data(path: Union[os.PathLike, str], download: bool = False) -> str:
 61    """Download the CTSpine1K dataset.
 62
 63    Args:
 64        path: Filepath to a folder where the data is downloaded for further processing.
 65        download: Whether to download the data if it is not present.
 66
 67    Returns:
 68        Filepath where the data is downloaded.
 69    """
 70    data_dir = os.path.join(path, "raw_data")
 71    if os.path.exists(data_dir) and glob(os.path.join(data_dir, "volumes", "*", "*.nii.gz")):
 72        return data_dir
 73
 74    if not download:
 75        raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.")
 76
 77    from huggingface_hub import snapshot_download
 78
 79    os.makedirs(path, exist_ok=True)
 80    snapshot_download(repo_id=REPO_ID, repo_type="dataset", local_dir=path)
 81    return data_dir
 82
 83
 84def get_ctspine1k_paths(
 85    path: Union[os.PathLike, str],
 86    source: Optional[Literal["colonog", "covid19", "hnscc", "msd_liver"]] = None,
 87    download: bool = False,
 88) -> Tuple[List[str], List[str]]:
 89    """Get paths to the CTSpine1K data.
 90
 91    Args:
 92        path: Filepath to a folder where the data is downloaded for further processing.
 93        source: The choice of source collection. All of them are used if it is not given.
 94        download: Whether to download the data if it is not present.
 95
 96    Returns:
 97        List of filepaths for the image data.
 98        List of filepaths for the label data.
 99    """
100    if source is not None and source not in SOURCES:
101        raise ValueError(f"'{source}' is not a valid source. Choose from {list(SOURCES.keys())}.")
102
103    data_dir = get_ctspine1k_data(path, download)
104
105    folders = list(SOURCES.values()) if source is None else [SOURCES[source]]
106    prepared_dir = os.path.join(path, "prepared")
107    raw_paths, label_paths = [], []
108    for folder in folders:
109        os.makedirs(os.path.join(prepared_dir, folder), exist_ok=True)
110        for image_path in natsorted(glob(os.path.join(data_dir, "volumes", folder, "*.nii.gz"))):
111            stem = os.path.basename(image_path)[:-len(".nii.gz")]
112            label_path = os.path.join(data_dir, "labels", folder, f"{stem}_seg.nii.gz")
113            if not os.path.exists(label_path):
114                continue
115            raw_paths.append(_prepare_name(image_path, prepared_dir, folder, stem))
116            label_paths.append(_prepare_name(label_path, prepared_dir, folder, f"{stem}_seg"))
117
118    assert len(raw_paths) == len(label_paths) and len(raw_paths) > 0
119
120    return raw_paths, label_paths
121
122
123def get_ctspine1k_dataset(
124    path: Union[os.PathLike, str],
125    patch_shape: Tuple[int, ...],
126    source: Optional[Literal["colonog", "covid19", "hnscc", "msd_liver"]] = None,
127    resize_inputs: bool = False,
128    download: bool = False,
129    **kwargs
130) -> Dataset:
131    """Get the CTSpine1K dataset for vertebra segmentation.
132
133    Args:
134        path: Filepath to a folder where the data is downloaded for further processing.
135        patch_shape: The patch shape to use for training.
136        source: The choice of source collection. All of them are used if it is not given.
137        resize_inputs: Whether to resize inputs to the desired patch shape.
138        download: Whether to download the data if it is not present.
139        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
140
141    Returns:
142        The segmentation dataset.
143    """
144    raw_paths, label_paths = get_ctspine1k_paths(path, source, download)
145
146    if resize_inputs:
147        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
148        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
149            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
150        )
151
152    return torch_em.default_segmentation_dataset(
153        raw_paths=raw_paths,
154        raw_key="data",
155        label_paths=label_paths,
156        label_key="data",
157        patch_shape=patch_shape,
158        is_seg_dataset=True,
159        **kwargs
160    )
161
162
163def get_ctspine1k_loader(
164    path: Union[os.PathLike, str],
165    batch_size: int,
166    patch_shape: Tuple[int, ...],
167    source: Optional[Literal["colonog", "covid19", "hnscc", "msd_liver"]] = None,
168    resize_inputs: bool = False,
169    download: bool = False,
170    **kwargs
171) -> DataLoader:
172    """Get the CTSpine1K dataloader for vertebra segmentation.
173
174    Args:
175        path: Filepath to a folder where the data is downloaded for further processing.
176        batch_size: The batch size for training.
177        patch_shape: The patch shape to use for training.
178        source: The choice of source collection. All of them are used if it is not given.
179        resize_inputs: Whether to resize inputs to the desired patch shape.
180        download: Whether to download the data if it is not present.
181        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
182
183    Returns:
184        The DataLoader.
185    """
186    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
187    dataset = get_ctspine1k_dataset(path, patch_shape, source, resize_inputs, download, **ds_kwargs)
188    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)
REPO_ID = 'alexanderdann/CTSpine1K'
SOURCES = {'colonog': 'COLONOG', 'covid19': 'COVID-19', 'hnscc': 'HNSCC-3DCT-RT', 'msd_liver': 'MSD-T10'}

Mapping from the source choice to its folder in the release.

CLASS_NAMES = ['c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 't1', 't2', 't3', 't4', 't5', 't6', 't7', 't8', 't9', 't10', 't11', 't12', 'l1', 'l2', 'l3', 'l4', 'l5', 'l6']

The vertebrae of the CTSpine1K dataset. The label id of a vertebra is its 1-based index.

CLASS_IDS = {'c1': 1, 'c2': 2, 'c3': 3, 'c4': 4, 'c5': 5, 'c6': 6, 'c7': 7, 't1': 8, 't2': 9, 't3': 10, 't4': 11, 't5': 12, 't6': 13, 't7': 14, 't8': 15, 't9': 16, 't10': 17, 't11': 18, 't12': 19, 'l1': 20, 'l2': 21, 'l3': 22, 'l4': 23, 'l5': 24, 'l6': 25}

Mapping from the vertebra name to its label id.

def get_ctspine1k_data(path: Union[os.PathLike, str], download: bool = False) -> str:
61def get_ctspine1k_data(path: Union[os.PathLike, str], download: bool = False) -> str:
62    """Download the CTSpine1K dataset.
63
64    Args:
65        path: Filepath to a folder where the data is downloaded for further processing.
66        download: Whether to download the data if it is not present.
67
68    Returns:
69        Filepath where the data is downloaded.
70    """
71    data_dir = os.path.join(path, "raw_data")
72    if os.path.exists(data_dir) and glob(os.path.join(data_dir, "volumes", "*", "*.nii.gz")):
73        return data_dir
74
75    if not download:
76        raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.")
77
78    from huggingface_hub import snapshot_download
79
80    os.makedirs(path, exist_ok=True)
81    snapshot_download(repo_id=REPO_ID, repo_type="dataset", local_dir=path)
82    return data_dir

Download the CTSpine1K 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 data is downloaded.

def get_ctspine1k_paths( path: Union[os.PathLike, str], source: Optional[Literal['colonog', 'covid19', 'hnscc', 'msd_liver']] = None, download: bool = False) -> Tuple[List[str], List[str]]:
 85def get_ctspine1k_paths(
 86    path: Union[os.PathLike, str],
 87    source: Optional[Literal["colonog", "covid19", "hnscc", "msd_liver"]] = None,
 88    download: bool = False,
 89) -> Tuple[List[str], List[str]]:
 90    """Get paths to the CTSpine1K data.
 91
 92    Args:
 93        path: Filepath to a folder where the data is downloaded for further processing.
 94        source: The choice of source collection. All of them are used if it is not given.
 95        download: Whether to download the data if it is not present.
 96
 97    Returns:
 98        List of filepaths for the image data.
 99        List of filepaths for the label data.
100    """
101    if source is not None and source not in SOURCES:
102        raise ValueError(f"'{source}' is not a valid source. Choose from {list(SOURCES.keys())}.")
103
104    data_dir = get_ctspine1k_data(path, download)
105
106    folders = list(SOURCES.values()) if source is None else [SOURCES[source]]
107    prepared_dir = os.path.join(path, "prepared")
108    raw_paths, label_paths = [], []
109    for folder in folders:
110        os.makedirs(os.path.join(prepared_dir, folder), exist_ok=True)
111        for image_path in natsorted(glob(os.path.join(data_dir, "volumes", folder, "*.nii.gz"))):
112            stem = os.path.basename(image_path)[:-len(".nii.gz")]
113            label_path = os.path.join(data_dir, "labels", folder, f"{stem}_seg.nii.gz")
114            if not os.path.exists(label_path):
115                continue
116            raw_paths.append(_prepare_name(image_path, prepared_dir, folder, stem))
117            label_paths.append(_prepare_name(label_path, prepared_dir, folder, f"{stem}_seg"))
118
119    assert len(raw_paths) == len(label_paths) and len(raw_paths) > 0
120
121    return raw_paths, label_paths

Get paths to the CTSpine1K data.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • source: The choice of source collection. All of them are used if it is not given.
  • 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_ctspine1k_dataset( path: Union[os.PathLike, str], patch_shape: Tuple[int, ...], source: Optional[Literal['colonog', 'covid19', 'hnscc', 'msd_liver']] = None, resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataset.Dataset:
124def get_ctspine1k_dataset(
125    path: Union[os.PathLike, str],
126    patch_shape: Tuple[int, ...],
127    source: Optional[Literal["colonog", "covid19", "hnscc", "msd_liver"]] = None,
128    resize_inputs: bool = False,
129    download: bool = False,
130    **kwargs
131) -> Dataset:
132    """Get the CTSpine1K dataset for vertebra segmentation.
133
134    Args:
135        path: Filepath to a folder where the data is downloaded for further processing.
136        patch_shape: The patch shape to use for training.
137        source: The choice of source collection. All of them are used if it is not given.
138        resize_inputs: Whether to resize inputs to the desired patch shape.
139        download: Whether to download the data if it is not present.
140        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
141
142    Returns:
143        The segmentation dataset.
144    """
145    raw_paths, label_paths = get_ctspine1k_paths(path, source, download)
146
147    if resize_inputs:
148        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
149        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
150            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
151        )
152
153    return torch_em.default_segmentation_dataset(
154        raw_paths=raw_paths,
155        raw_key="data",
156        label_paths=label_paths,
157        label_key="data",
158        patch_shape=patch_shape,
159        is_seg_dataset=True,
160        **kwargs
161    )

Get the CTSpine1K dataset for vertebra segmentation.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • patch_shape: The patch shape to use for training.
  • source: The choice of source collection. All of them are used if it is not given.
  • 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_ctspine1k_loader( path: Union[os.PathLike, str], batch_size: int, patch_shape: Tuple[int, ...], source: Optional[Literal['colonog', 'covid19', 'hnscc', 'msd_liver']] = None, resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataloader.DataLoader:
164def get_ctspine1k_loader(
165    path: Union[os.PathLike, str],
166    batch_size: int,
167    patch_shape: Tuple[int, ...],
168    source: Optional[Literal["colonog", "covid19", "hnscc", "msd_liver"]] = None,
169    resize_inputs: bool = False,
170    download: bool = False,
171    **kwargs
172) -> DataLoader:
173    """Get the CTSpine1K dataloader for vertebra segmentation.
174
175    Args:
176        path: Filepath to a folder where the data is downloaded for further processing.
177        batch_size: The batch size for training.
178        patch_shape: The patch shape to use for training.
179        source: The choice of source collection. All of them are used if it is not given.
180        resize_inputs: Whether to resize inputs to the desired patch shape.
181        download: Whether to download the data if it is not present.
182        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
183
184    Returns:
185        The DataLoader.
186    """
187    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
188    dataset = get_ctspine1k_dataset(path, patch_shape, source, resize_inputs, download, **ds_kwargs)
189    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)

Get the CTSpine1K dataloader for vertebra 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.
  • source: The choice of source collection. All of them are used if it is not given.
  • 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.