torch_em.data.datasets.medical.skm_tea

The SKM-TEA dataset contains annotations for knee tissue segmentation in quantitative double-echo steady-state (qDESS) knee MRI.

It comprises 155 scans with two echoes each and dense segmentations of six soft tissue classes. The scans come with an official train / val / test split. The label ids are:

  • 0: background
  • 1: patellar cartilage
  • 2: femoral cartilage
  • 3: tibial cartilage (medial)
  • 4: tibial cartilage (lateral)
  • 5: meniscus (medial)
  • 6: meniscus (lateral)

NOTE: The label ids were verified against the official dataset documentation (https://github.com/StanfordMIMI/skm-tea/blob/main/DATASET.md), which documents the order of the one-hot encoded 'seg' volumes, and on the data: the six channels do not overlap and the by far largest one is the femoral cartilage.

NOTE: The full dataset with all 155 scans requires registration and cannot be downloaded automatically. Only the official sample of 3 scans (one per split), which the authors published at https://huggingface.co/datasets/arjundd/skm-tea-mini, is downloaded if download is set to True. Note that this sample must not be used for reporting metrics. To use the full dataset, follow these steps:

  • Visit https://aimi.stanford.edu/skm-tea-knee-mri and follow the link to the dataset on the Stanford AIMI shared datasets portal (https://stanfordaimi.azurewebsites.net/datasets/4aaeafb9-c6e6-4e3c-9188-3aaaf0e0a9e7).
  • Log in or create an account and follow the download instructions (the full dataset is ~900 GB compressed, but only the 'DICOM' track ('image_files') and the annotations are required here).
  • Extract the data such that '/image_files/.h5' and '/annotations//{train,val,test}.json' exist, i.e. '' is the 'skm-tea' folder described in https://github.com/StanfordMIMI/skm-tea/blob/main/DATASET.md.

The dataset is located at https://aimi.stanford.edu/skm-tea-knee-mri.

This dataset is from the publication https://openreview.net/forum?id=YDMFgD_qJuA. Please cite it if you use this dataset in your research.

  1"""The SKM-TEA dataset contains annotations for knee tissue segmentation in quantitative double-echo
  2steady-state (qDESS) knee MRI.
  3
  4It comprises 155 scans with two echoes each and dense segmentations of six soft tissue classes. The scans
  5come with an official train / val / test split. The label ids are:
  6- 0: background
  7- 1: patellar cartilage
  8- 2: femoral cartilage
  9- 3: tibial cartilage (medial)
 10- 4: tibial cartilage (lateral)
 11- 5: meniscus (medial)
 12- 6: meniscus (lateral)
 13
 14NOTE: The label ids were verified against the official dataset documentation
 15(https://github.com/StanfordMIMI/skm-tea/blob/main/DATASET.md), which documents the order of the one-hot
 16encoded 'seg' volumes, and on the data: the six channels do not overlap and the by far largest one is the
 17femoral cartilage.
 18
 19NOTE: The full dataset with all 155 scans requires registration and cannot be downloaded automatically.
 20Only the official sample of 3 scans (one per split), which the authors published at
 21https://huggingface.co/datasets/arjundd/skm-tea-mini, is downloaded if `download` is set to True.
 22Note that this sample must not be used for reporting metrics. To use the full dataset, follow these steps:
 23- Visit https://aimi.stanford.edu/skm-tea-knee-mri and follow the link to the dataset on the Stanford AIMI
 24  shared datasets portal (https://stanfordaimi.azurewebsites.net/datasets/4aaeafb9-c6e6-4e3c-9188-3aaaf0e0a9e7).
 25- Log in or create an account and follow the download instructions (the full dataset is ~900 GB compressed,
 26  but only the 'DICOM' track ('image_files') and the annotations are required here).
 27- Extract the data such that '<path>/image_files/<scan_id>.h5' and
 28  '<path>/annotations/<version>/{train,val,test}.json' exist, i.e. '<path>' is the 'skm-tea' folder described
 29  in https://github.com/StanfordMIMI/skm-tea/blob/main/DATASET.md.
 30
 31The dataset is located at https://aimi.stanford.edu/skm-tea-knee-mri.
 32
 33This dataset is from the publication https://openreview.net/forum?id=YDMFgD_qJuA.
 34Please cite it if you use this dataset in your research.
 35"""
 36
 37import os
 38import json
 39import warnings
 40from glob import glob
 41from tqdm import tqdm
 42from natsort import natsorted
 43from typing import Union, Tuple, Literal, List
 44
 45import numpy as np
 46
 47from torch.utils.data import Dataset, DataLoader
 48
 49import torch_em
 50
 51from .. import util
 52
 53
 54SAMPLE_URL = "https://huggingface.co/datasets/arjundd/skm-tea-mini/resolve/main/v1-release"
 55
 56SAMPLE_CHECKSUMS = {
 57    "image_files.tar.gz": "18af509e31f4468dbb5b237bf1a124508d00ade4ace0b90fae62947ce4709812",
 58    "train.json": "7a2b80f6ecae2eabe2489235302ac0003ac39f1266c4d356abbf5279f0ccdb7a",
 59    "val.json": "5d64909b69f89a101755ace309c4769c5884758957efa35d4da27b3b4b455986",
 60    "test.json": "b057cf984d7fcdd52318642ea2ec3a9241464575c3a91dc29f64e4fef51dad74",
 61}
 62
 63SAMPLE_VERSION = "v1.0.0"
 64
 65LABEL_IDS = {
 66    "background": 0,
 67    "patellar_cartilage": 1,
 68    "femoral_cartilage": 2,
 69    "tibial_cartilage_medial": 3,
 70    "tibial_cartilage_lateral": 4,
 71    "meniscus_medial": 5,
 72    "meniscus_lateral": 6,
 73}
 74
 75
 76def _preprocess_inputs(path, image_dir):
 77    import h5py
 78
 79    preprocessed_dir = os.path.join(path, "preprocessed")
 80    os.makedirs(preprocessed_dir, exist_ok=True)
 81
 82    for scan_path in tqdm(natsorted(glob(os.path.join(image_dir, "*.h5"))), desc="Preprocessing the SKM-TEA scans"):
 83        volume_path = os.path.join(preprocessed_dir, os.path.basename(scan_path))
 84        if os.path.exists(volume_path):
 85            continue
 86
 87        with h5py.File(scan_path, "r") as f:
 88            echo1, echo2, seg = f["echo1"][:], f["echo2"][:], f["seg"][:]
 89
 90        # The one-hot encoded segmentation (X, Y, Z, 6) is converted to a label volume with the ids 1 to 6.
 91        labels = np.where(seg.any(axis=-1), np.argmax(seg, axis=-1) + 1, 0).astype(np.uint8)
 92
 93        # The volumes are stored as (X, Y, Z) with Z (LR) being the sagittal slice direction.
 94        # We move the slice axis to the front, so that 2d training samples sagittal slices.
 95        # The file is written to a temporary path first, so that an interrupted run does not leave a corrupt file.
 96        with h5py.File(f"{volume_path}.tmp", "w") as f:
 97            f.create_dataset("raw/echo1", data=echo1.transpose(2, 0, 1), compression="gzip")
 98            f.create_dataset("raw/echo2", data=echo2.transpose(2, 0, 1), compression="gzip")
 99            rss = np.sqrt(echo1.astype("float32") ** 2 + echo2.astype("float32") ** 2)
100            f.create_dataset("raw/rss", data=rss.transpose(2, 0, 1), compression="gzip")
101            f.create_dataset("labels", data=labels.transpose(2, 0, 1), compression="gzip")
102
103        os.rename(f"{volume_path}.tmp", volume_path)
104
105    return preprocessed_dir
106
107
108def _download_sample_data(path, download):
109    msg = "Only the official sample of 3 SKM-TEA scans is downloaded, not the full dataset with 155 scans. "
110    msg += "See 'torch_em.data.datasets.medical.skm_tea' for how to obtain the full dataset."
111    warnings.warn(msg)
112
113    os.makedirs(path, exist_ok=True)
114
115    tar_path = os.path.join(path, "image_files.tar.gz")
116    util.download_source(
117        path=tar_path,
118        url=f"{SAMPLE_URL}/tarball/image_files.tar.gz",
119        download=download,
120        checksum=SAMPLE_CHECKSUMS["image_files.tar.gz"],
121    )
122    util.unzip_tarfile(tar_path=tar_path, dst=path)
123
124    annotation_dir = os.path.join(path, "annotations", SAMPLE_VERSION)
125    os.makedirs(annotation_dir, exist_ok=True)
126    for split in ["train", "val", "test"]:
127        split_path = os.path.join(annotation_dir, f"{split}.json")
128        if os.path.exists(split_path):
129            continue
130
131        util.download_source(
132            path=split_path,
133            url=f"{SAMPLE_URL}/annotations/{SAMPLE_VERSION}/{split}.json",
134            download=download,
135            checksum=SAMPLE_CHECKSUMS[f"{split}.json"],
136        )
137
138
139def get_skm_tea_data(path: Union[os.PathLike, str], download: bool = False) -> str:
140    """Download the SKM-TEA dataset.
141
142    Args:
143        path: Filepath to a folder where the data is downloaded for further processing.
144        download: Whether to download the data if it is not present.
145
146    Returns:
147        Filepath where the data is preprocessed.
148    """
149    preprocessed_dir = os.path.join(path, "preprocessed")
150    if os.path.exists(preprocessed_dir) and len(glob(os.path.join(preprocessed_dir, "*.h5"))) > 0:
151        return preprocessed_dir
152
153    image_dir = os.path.join(path, "image_files")
154    if not os.path.exists(image_dir):
155        if not download:
156            raise FileNotFoundError(
157                f"It's expected to place the downloaded SKM-TEA 'image_files' folder at '{image_dir}'. "
158                "See 'torch_em.data.datasets.medical.skm_tea' for the manual download instructions."
159            )
160
161        _download_sample_data(path, download)
162
163    return _preprocess_inputs(path, image_dir)
164
165
166def get_skm_tea_paths(
167    path: Union[os.PathLike, str],
168    split: Literal["train", "val", "test"],
169    version: str = "v1.0.0",
170    download: bool = False,
171) -> Tuple[List[str], List[str]]:
172    """Get paths to the SKM-TEA data.
173
174    Args:
175        path: Filepath to a folder where the data is downloaded for further processing.
176        split: The choice of data split.
177        version: The version of the annotations that define the split.
178        download: Whether to download the data if it is not present.
179
180    Returns:
181        List of filepaths for the image data.
182        List of filepaths for the label data.
183    """
184    data_dir = get_skm_tea_data(path, download)
185
186    if split not in ["train", "val", "test"]:
187        raise ValueError(f"'{split}' is not a valid split.")
188
189    split_path = os.path.join(path, "annotations", version, f"{split}.json")
190    if not os.path.exists(split_path):
191        raise FileNotFoundError(f"Could not find the annotation file for the '{split}' split at '{split_path}'.")
192
193    with open(split_path, "r") as f:
194        scan_ids = [image["scan_id"] for image in json.load(f)["images"]]
195
196    volume_paths = natsorted([os.path.join(data_dir, f"{scan_id}.h5") for scan_id in scan_ids])
197    volume_paths = [p for p in volume_paths if os.path.exists(p)]
198    assert len(volume_paths) > 0, f"Could not find any preprocessed scans for the '{split}' split."
199
200    return volume_paths, volume_paths
201
202
203def get_skm_tea_dataset(
204    path: Union[os.PathLike, str],
205    patch_shape: Tuple[int, ...],
206    split: Literal["train", "val", "test"],
207    echo: Literal["echo1", "echo2", "rss"] = "echo1",
208    resize_inputs: bool = False,
209    download: bool = False,
210    **kwargs
211) -> Dataset:
212    """Get the SKM-TEA dataset for knee tissue segmentation.
213
214    Args:
215        path: Filepath to a folder where the data is downloaded for further processing.
216        patch_shape: The patch shape to use for training.
217        split: The choice of data split.
218        echo: The qDESS echo to use as input. Either 'echo1', 'echo2' or 'rss' (root-sum-of-squares of both).
219        resize_inputs: Whether to resize inputs to the desired patch shape.
220        download: Whether to download the data if it is not present.
221        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
222
223    Returns:
224        The segmentation dataset.
225    """
226    if echo not in ["echo1", "echo2", "rss"]:
227        raise ValueError(f"'{echo}' is not a valid echo. Choose one of 'echo1', 'echo2' or 'rss'.")
228
229    raw_paths, label_paths = get_skm_tea_paths(path, split, download=download)
230
231    if resize_inputs:
232        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
233        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
234            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
235        )
236
237    return torch_em.default_segmentation_dataset(
238        raw_paths=raw_paths,
239        raw_key=f"raw/{echo}",
240        label_paths=label_paths,
241        label_key="labels",
242        patch_shape=patch_shape,
243        is_seg_dataset=True,
244        **kwargs
245    )
246
247
248def get_skm_tea_loader(
249    path: Union[os.PathLike, str],
250    batch_size: int,
251    patch_shape: Tuple[int, ...],
252    split: Literal["train", "val", "test"],
253    echo: Literal["echo1", "echo2", "rss"] = "echo1",
254    resize_inputs: bool = False,
255    download: bool = False,
256    **kwargs
257) -> DataLoader:
258    """Get the SKM-TEA dataloader for knee tissue segmentation.
259
260    Args:
261        path: Filepath to a folder where the data is downloaded for further processing.
262        batch_size: The batch size for training.
263        patch_shape: The patch shape to use for training.
264        split: The choice of data split.
265        echo: The qDESS echo to use as input. Either 'echo1', 'echo2' or 'rss' (root-sum-of-squares of both).
266        resize_inputs: Whether to resize inputs to the desired patch shape.
267        download: Whether to download the data if it is not present.
268        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
269
270    Returns:
271        The DataLoader.
272    """
273    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
274    dataset = get_skm_tea_dataset(path, patch_shape, split, echo, resize_inputs, download, **ds_kwargs)
275    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)
SAMPLE_URL = 'https://huggingface.co/datasets/arjundd/skm-tea-mini/resolve/main/v1-release'
SAMPLE_CHECKSUMS = {'image_files.tar.gz': '18af509e31f4468dbb5b237bf1a124508d00ade4ace0b90fae62947ce4709812', 'train.json': '7a2b80f6ecae2eabe2489235302ac0003ac39f1266c4d356abbf5279f0ccdb7a', 'val.json': '5d64909b69f89a101755ace309c4769c5884758957efa35d4da27b3b4b455986', 'test.json': 'b057cf984d7fcdd52318642ea2ec3a9241464575c3a91dc29f64e4fef51dad74'}
SAMPLE_VERSION = 'v1.0.0'
LABEL_IDS = {'background': 0, 'patellar_cartilage': 1, 'femoral_cartilage': 2, 'tibial_cartilage_medial': 3, 'tibial_cartilage_lateral': 4, 'meniscus_medial': 5, 'meniscus_lateral': 6}
def get_skm_tea_data(path: Union[os.PathLike, str], download: bool = False) -> str:
140def get_skm_tea_data(path: Union[os.PathLike, str], download: bool = False) -> str:
141    """Download the SKM-TEA dataset.
142
143    Args:
144        path: Filepath to a folder where the data is downloaded for further processing.
145        download: Whether to download the data if it is not present.
146
147    Returns:
148        Filepath where the data is preprocessed.
149    """
150    preprocessed_dir = os.path.join(path, "preprocessed")
151    if os.path.exists(preprocessed_dir) and len(glob(os.path.join(preprocessed_dir, "*.h5"))) > 0:
152        return preprocessed_dir
153
154    image_dir = os.path.join(path, "image_files")
155    if not os.path.exists(image_dir):
156        if not download:
157            raise FileNotFoundError(
158                f"It's expected to place the downloaded SKM-TEA 'image_files' folder at '{image_dir}'. "
159                "See 'torch_em.data.datasets.medical.skm_tea' for the manual download instructions."
160            )
161
162        _download_sample_data(path, download)
163
164    return _preprocess_inputs(path, image_dir)

Download the SKM-TEA 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 preprocessed.

def get_skm_tea_paths( path: Union[os.PathLike, str], split: Literal['train', 'val', 'test'], version: str = 'v1.0.0', download: bool = False) -> Tuple[List[str], List[str]]:
167def get_skm_tea_paths(
168    path: Union[os.PathLike, str],
169    split: Literal["train", "val", "test"],
170    version: str = "v1.0.0",
171    download: bool = False,
172) -> Tuple[List[str], List[str]]:
173    """Get paths to the SKM-TEA data.
174
175    Args:
176        path: Filepath to a folder where the data is downloaded for further processing.
177        split: The choice of data split.
178        version: The version of the annotations that define the split.
179        download: Whether to download the data if it is not present.
180
181    Returns:
182        List of filepaths for the image data.
183        List of filepaths for the label data.
184    """
185    data_dir = get_skm_tea_data(path, download)
186
187    if split not in ["train", "val", "test"]:
188        raise ValueError(f"'{split}' is not a valid split.")
189
190    split_path = os.path.join(path, "annotations", version, f"{split}.json")
191    if not os.path.exists(split_path):
192        raise FileNotFoundError(f"Could not find the annotation file for the '{split}' split at '{split_path}'.")
193
194    with open(split_path, "r") as f:
195        scan_ids = [image["scan_id"] for image in json.load(f)["images"]]
196
197    volume_paths = natsorted([os.path.join(data_dir, f"{scan_id}.h5") for scan_id in scan_ids])
198    volume_paths = [p for p in volume_paths if os.path.exists(p)]
199    assert len(volume_paths) > 0, f"Could not find any preprocessed scans for the '{split}' split."
200
201    return volume_paths, volume_paths

Get paths to the SKM-TEA data.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • split: The choice of data split.
  • version: The version of the annotations that define the split.
  • 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_skm_tea_dataset( path: Union[os.PathLike, str], patch_shape: Tuple[int, ...], split: Literal['train', 'val', 'test'], echo: Literal['echo1', 'echo2', 'rss'] = 'echo1', resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataset.Dataset:
204def get_skm_tea_dataset(
205    path: Union[os.PathLike, str],
206    patch_shape: Tuple[int, ...],
207    split: Literal["train", "val", "test"],
208    echo: Literal["echo1", "echo2", "rss"] = "echo1",
209    resize_inputs: bool = False,
210    download: bool = False,
211    **kwargs
212) -> Dataset:
213    """Get the SKM-TEA dataset for knee tissue segmentation.
214
215    Args:
216        path: Filepath to a folder where the data is downloaded for further processing.
217        patch_shape: The patch shape to use for training.
218        split: The choice of data split.
219        echo: The qDESS echo to use as input. Either 'echo1', 'echo2' or 'rss' (root-sum-of-squares of both).
220        resize_inputs: Whether to resize inputs to the desired patch shape.
221        download: Whether to download the data if it is not present.
222        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
223
224    Returns:
225        The segmentation dataset.
226    """
227    if echo not in ["echo1", "echo2", "rss"]:
228        raise ValueError(f"'{echo}' is not a valid echo. Choose one of 'echo1', 'echo2' or 'rss'.")
229
230    raw_paths, label_paths = get_skm_tea_paths(path, split, download=download)
231
232    if resize_inputs:
233        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
234        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
235            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
236        )
237
238    return torch_em.default_segmentation_dataset(
239        raw_paths=raw_paths,
240        raw_key=f"raw/{echo}",
241        label_paths=label_paths,
242        label_key="labels",
243        patch_shape=patch_shape,
244        is_seg_dataset=True,
245        **kwargs
246    )

Get the SKM-TEA dataset for knee tissue 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.
  • echo: The qDESS echo to use as input. Either 'echo1', 'echo2' or 'rss' (root-sum-of-squares of both).
  • 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_skm_tea_loader( path: Union[os.PathLike, str], batch_size: int, patch_shape: Tuple[int, ...], split: Literal['train', 'val', 'test'], echo: Literal['echo1', 'echo2', 'rss'] = 'echo1', resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataloader.DataLoader:
249def get_skm_tea_loader(
250    path: Union[os.PathLike, str],
251    batch_size: int,
252    patch_shape: Tuple[int, ...],
253    split: Literal["train", "val", "test"],
254    echo: Literal["echo1", "echo2", "rss"] = "echo1",
255    resize_inputs: bool = False,
256    download: bool = False,
257    **kwargs
258) -> DataLoader:
259    """Get the SKM-TEA dataloader for knee tissue segmentation.
260
261    Args:
262        path: Filepath to a folder where the data is downloaded for further processing.
263        batch_size: The batch size for training.
264        patch_shape: The patch shape to use for training.
265        split: The choice of data split.
266        echo: The qDESS echo to use as input. Either 'echo1', 'echo2' or 'rss' (root-sum-of-squares of both).
267        resize_inputs: Whether to resize inputs to the desired patch shape.
268        download: Whether to download the data if it is not present.
269        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
270
271    Returns:
272        The DataLoader.
273    """
274    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
275    dataset = get_skm_tea_dataset(path, patch_shape, split, echo, resize_inputs, download, **ds_kwargs)
276    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)

Get the SKM-TEA dataloader for knee tissue 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.
  • echo: The qDESS echo to use as input. Either 'echo1', 'echo2' or 'rss' (root-sum-of-squares of both).
  • 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.