torch_em.data.datasets.medical.scd

The SCD dataset contains annotations for left ventricular endocardium and epicardium segmentation in short-axis cardiac cine-MRI.

The Sunnybrook Cardiac Data (SCD), also known as the 2009 Cardiac MR Left Ventricle Segmentation Challenge data, consists of 45 cine-MRI studies from a mix of patients and pathologies: healthy, hypertrophy, heart failure with infarction and heart failure without infarction. Manual contours of the left ventricular endocardium and epicardium are provided for the end-diastolic and end-systolic frames (and, for a subset of slices, further frames), drawn by an expert cardiologist. The dataset is located at https://www.cardiacatlas.org/sunnybrook-cardiac-data/ and is distributed under the CC0 1.0 Universal license.

The labels are multiclass, see LABEL_IDS: 1 = left ventricular cavity (endocardium), 2 = left ventricular myocardium (the region between the epicardial and endocardial contour). Slices without an epicardial contour only have the cavity label.

This dataset is from the publication http://hdl.handle.net/10380/3070. Please cite it if you use this dataset for your research.

  1"""The SCD dataset contains annotations for left ventricular endocardium and epicardium
  2segmentation in short-axis cardiac cine-MRI.
  3
  4The Sunnybrook Cardiac Data (SCD), also known as the 2009 Cardiac MR Left Ventricle Segmentation
  5Challenge data, consists of 45 cine-MRI studies from a mix of patients and pathologies: healthy,
  6hypertrophy, heart failure with infarction and heart failure without infarction. Manual contours
  7of the left ventricular endocardium and epicardium are provided for the end-diastolic and
  8end-systolic frames (and, for a subset of slices, further frames), drawn by an expert
  9cardiologist. The dataset is located at https://www.cardiacatlas.org/sunnybrook-cardiac-data/
 10and is distributed under the CC0 1.0 Universal license.
 11
 12The labels are multiclass, see `LABEL_IDS`: 1 = left ventricular cavity (endocardium), 2 = left
 13ventricular myocardium (the region between the epicardial and endocardial contour). Slices without
 14an epicardial contour only have the cavity label.
 15
 16This dataset is from the publication http://hdl.handle.net/10380/3070.
 17Please cite it if you use this dataset for your research.
 18"""
 19
 20import os
 21import re
 22from glob import glob
 23from tqdm import tqdm
 24from natsort import natsorted
 25from typing import Union, Tuple, List
 26
 27import numpy as np
 28from skimage.draw import polygon
 29
 30from torch.utils.data import Dataset, DataLoader
 31
 32import torch_em
 33
 34from .. import util
 35
 36
 37URLS = {
 38    "images_1": "https://www.cardiacatlas.org/share/download.php?id=98&token=kUVtCTMzfjlHwunzFYST7yNGFnlUtV7W&download",  # noqa
 39    "images_2": "https://www.cardiacatlas.org/share/download.php?id=99&token=2euh4WH03DynHi9Y5elb2HSWJaJIeWER&download",  # noqa
 40    "images_3": "https://www.cardiacatlas.org/share/download.php?id=100&token=eT37AbXslu1JQp1GSZy2BmmWzWmXDdfx&download",  # noqa
 41    "images_4": "https://www.cardiacatlas.org/share/download.php?id=101&token=Qn0dY2lHTyTIBF5qNHRkbHdUSnWnS4Yy&download",  # noqa
 42    "images_5": "https://www.cardiacatlas.org/share/download.php?id=102&token=nYALJl6kS6cj5jgK9R8CKDpgQwZn1pm3&download",  # noqa
 43    "contours": "https://www.cardiacatlas.org/share/download.php?id=61&token=gFjv8psvCQu2vDYldUW9TYUIDic11fDt&download",  # noqa
 44    "patient_data": "https://www.cardiacatlas.org/share/download.php?id=66&token=Y1D66ieUdbXFlqmZ4icHJheJy44MXUPY&download",  # noqa
 45}
 46
 47CHECKSUMS = {
 48    "images_1": "708ce04db1ac33948a00b9052d44e9548c6807121a4841f4c35080d6db127b72",
 49    "images_2": "ea97bd11dfc1154bdcf02b6466c00637bc7e0dc04b5ddc3d94d68a7cd7a3e6a5",
 50    "images_3": "5b065fadef1b523bbabfec0d85327f375f743f2dd72810a5927b21b834b45deb",
 51    "images_4": "2a8d7054d9b89b5af4c9764b4b04eecee57b61b71b69f5ac69ec12629e9a615c",
 52    "images_5": "e65a818fe89c665c344317222a052360e210936c81f89ceb9d36e8e3c6e2d6e9",
 53    "contours": "93d8e06dfa53fc384e78810aeac86c336b5bdecbff1c9c175ec9254851cc2162",
 54    "patient_data": "c39eb11924d021e8ec51bb984daa57d1adadcc8eb7e342640181ce151dae5ded",
 55}
 56
 57LABEL_IDS = {"background": 0, "cavity": 1, "myocardium": 2}
 58
 59
 60def _normalize_original_id(original_id):
 61    """Zero-pad the trailing case number of an 'OriginalID' to match the contour directory naming,
 62    e.g. 'SC-HF-I-1' -> 'SC-HF-I-01'."""
 63    return re.sub(r"-(\d+)$", lambda m: f"-{int(m.group(1)):02d}", original_id)
 64
 65
 66def _patient_id_mapping(patient_csv_path):
 67    import csv
 68
 69    mapping = {}
 70    with open(patient_csv_path, newline="", encoding="utf-8-sig") as f:
 71        for row in csv.DictReader(f):
 72            mapping[_normalize_original_id(row["OriginalID"])] = row["PatientID"]
 73    return mapping
 74
 75
 76def _parse_contour(contour_path):
 77    points = np.loadtxt(contour_path)
 78    return points[:, 0], points[:, 1]
 79
 80
 81INSTANCE_PATTERN = re.compile(r"IM-\d+-(\d+)-[io]contour-manual\.txt$")
 82
 83
 84def _index_cinesax_instances(patient_dir):
 85    """Index the DICOM instances of the short-axis cine series ('CINESAX') of a patient folder by their
 86    instance number. The Cardiac Atlas Project redistribution stores several series per patient (long-axis
 87    cines, scouts, perfusion, ...) under arbitrarily numbered subfolders, and only the DICOM header's
 88    'SeriesDescription' reliably identifies the short-axis cine series that the manual contours refer to;
 89    the numeric prefix of the contour filenames (e.g. 'IM-0001-0048') does not correspond to the subfolder
 90    or series naming of this redistribution."""
 91    import pydicom
 92
 93    instances = {}
 94    for series_dir in sorted(p for p in glob(os.path.join(patient_dir, "*")) if os.path.isdir(p)):
 95        dicom_paths = natsorted(glob(os.path.join(series_dir, "*.dcm")))
 96        if not dicom_paths:
 97            continue
 98
 99        header = pydicom.dcmread(dicom_paths[0], stop_before_pixels=True)
100        if "CINESAX" not in getattr(header, "SeriesDescription", "").upper():
101            continue
102
103        for dicom_path in dicom_paths:
104            match = re.search(r"-(\d+)\.dcm$", os.path.basename(dicom_path))
105            if match is None:
106                continue
107            instances.setdefault(int(match.group(1)), dicom_path)
108
109    return instances
110
111
112def _rasterize_case(instances, contour_dir, preprocessed_dir, patient_id):
113    import pydicom
114    import imageio.v3 as imageio
115
116    icontours = natsorted(glob(os.path.join(contour_dir, "*-icontour-manual.txt")))
117
118    for icontour_path in icontours:
119        match = INSTANCE_PATTERN.search(os.path.basename(icontour_path))
120        if match is None or int(match.group(1)) not in instances:
121            continue
122
123        img_no = int(match.group(1))
124        stem = f"{img_no:04}"
125        gt_path = os.path.join(preprocessed_dir, f"{patient_id}_{stem}.tif")
126        raw_path = os.path.join(preprocessed_dir, f"{patient_id}_{stem}_raw.tif")
127        if os.path.exists(gt_path) and os.path.exists(raw_path):
128            continue
129
130        dcm = pydicom.dcmread(instances[img_no])
131        image = np.asarray(dcm.pixel_array)
132        shape = image.shape
133
134        labels = np.zeros(shape, dtype="uint8")
135
136        ocontour_path = os.path.join(contour_dir, os.path.basename(icontour_path).replace("icontour", "ocontour"))
137        if os.path.exists(ocontour_path):
138            x, y = _parse_contour(ocontour_path)
139            r, c = polygon(y, x, shape=shape)
140            labels[r, c] = LABEL_IDS["myocardium"]
141
142        x, y = _parse_contour(icontour_path)
143        r, c = polygon(y, x, shape=shape)
144        labels[r, c] = LABEL_IDS["cavity"]
145
146        imageio.imwrite(raw_path, image)
147        imageio.imwrite(gt_path, labels)
148
149
150def _preprocess_inputs(path, preprocessed_dir):
151    os.makedirs(preprocessed_dir, exist_ok=True)
152
153    patient_csv_path = os.path.join(path, "scd_patientdata.csv")
154    mapping = _patient_id_mapping(patient_csv_path)
155
156    contours_root = os.path.join(path, "SCD_ManualContours")
157    case_dirs = natsorted(glob(os.path.join(contours_root, "SC-*")))
158
159    for case_dir in tqdm(case_dirs, desc="Preprocessing the SCD studies"):
160        original_id = os.path.basename(case_dir)
161        patient_id = mapping.get(original_id)
162        if patient_id is None:
163            continue
164
165        contour_dir = os.path.join(case_dir, "contours-manual", "IRCCI-expert")
166        if not os.path.exists(contour_dir):
167            continue
168
169        patient_dir = os.path.join(path, patient_id)
170        instances = _index_cinesax_instances(patient_dir)
171        if not instances:
172            continue
173
174        _rasterize_case(instances, contour_dir, preprocessed_dir, patient_id)
175
176
177def get_scd_data(path: Union[os.PathLike, str], download: bool = False) -> str:
178    """Download the SCD dataset.
179
180    Args:
181        path: Filepath to a folder where the data is downloaded for further processing.
182        download: Whether to download the data if it is not present.
183
184    Returns:
185        Filepath where the preprocessed data is stored.
186    """
187    preprocessed_dir = os.path.join(path, "preprocessed")
188    if glob(os.path.join(preprocessed_dir, "*_raw.tif")):
189        return preprocessed_dir
190
191    os.makedirs(path, exist_ok=True)
192
193    for key in ["images_1", "images_2", "images_3", "images_4", "images_5"]:
194        zip_path = os.path.join(path, f"{key}.zip")
195        util.download_source(
196            path=zip_path, url=URLS[key], download=download, checksum=CHECKSUMS[key], verify=False
197        )
198        util.unzip(zip_path=zip_path, dst=path)
199
200    contours_dir = os.path.join(path, "SCD_ManualContours")
201    if not os.path.exists(contours_dir):
202        zip_path = os.path.join(path, "contours.zip")
203        util.download_source(
204            path=zip_path, url=URLS["contours"], download=download, checksum=CHECKSUMS["contours"], verify=False
205        )
206        util.unzip(zip_path=zip_path, dst=path)
207
208    patient_csv_path = os.path.join(path, "scd_patientdata.csv")
209    util.download_source(
210        path=patient_csv_path,
211        url=URLS["patient_data"],
212        download=download,
213        checksum=CHECKSUMS["patient_data"],
214        verify=False,
215    )
216
217    _preprocess_inputs(path, preprocessed_dir)
218    return preprocessed_dir
219
220
221def get_scd_paths(path: Union[os.PathLike, str], download: bool = False) -> Tuple[List[str], List[str]]:
222    """Get paths to the SCD data.
223
224    Args:
225        path: Filepath to a folder where the data is downloaded for further processing.
226        download: Whether to download the data if it is not present.
227
228    Returns:
229        List of filepaths for the image data.
230        List of filepaths for the label data.
231    """
232    data_dir = get_scd_data(path, download)
233
234    gt_paths = natsorted(glob(os.path.join(data_dir, "*.tif")))
235    gt_paths = [p for p in gt_paths if not p.endswith("_raw.tif")]
236    image_paths = [p.replace(".tif", "_raw.tif") for p in gt_paths]
237
238    return image_paths, gt_paths
239
240
241def get_scd_dataset(
242    path: Union[os.PathLike, str],
243    patch_shape: Tuple[int, int],
244    resize_inputs: bool = False,
245    download: bool = False,
246    **kwargs
247) -> Dataset:
248    """Get the SCD dataset for left ventricular cavity and myocardium segmentation.
249
250    Args:
251        path: Filepath to a folder where the data is downloaded for further processing.
252        patch_shape: The patch shape to use for training.
253        resize_inputs: Whether to resize the inputs to the expected patch shape.
254        download: Whether to download the data if it is not present.
255        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
256
257    Returns:
258        The segmentation dataset.
259    """
260    image_paths, gt_paths = get_scd_paths(path, download)
261
262    if resize_inputs:
263        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
264        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
265            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
266        )
267
268    return torch_em.default_segmentation_dataset(
269        raw_paths=image_paths,
270        raw_key=None,
271        label_paths=gt_paths,
272        label_key=None,
273        patch_shape=patch_shape,
274        is_seg_dataset=False,
275        **kwargs
276    )
277
278
279def get_scd_loader(
280    path: Union[os.PathLike, str],
281    batch_size: int,
282    patch_shape: Tuple[int, int],
283    resize_inputs: bool = False,
284    download: bool = False,
285    **kwargs
286) -> DataLoader:
287    """Get the SCD dataloader for left ventricular cavity and myocardium segmentation.
288
289    Args:
290        path: Filepath to a folder where the data is downloaded for further processing.
291        batch_size: The batch size for training.
292        patch_shape: The patch shape to use for training.
293        resize_inputs: Whether to resize the inputs to the expected patch shape.
294        download: Whether to download the data if it is not present.
295        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
296
297    Returns:
298        The DataLoader.
299    """
300    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
301    dataset = get_scd_dataset(path, patch_shape, resize_inputs, download, **ds_kwargs)
302    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)
URLS = {'images_1': 'https://www.cardiacatlas.org/share/download.php?id=98&token=kUVtCTMzfjlHwunzFYST7yNGFnlUtV7W&download', 'images_2': 'https://www.cardiacatlas.org/share/download.php?id=99&token=2euh4WH03DynHi9Y5elb2HSWJaJIeWER&download', 'images_3': 'https://www.cardiacatlas.org/share/download.php?id=100&token=eT37AbXslu1JQp1GSZy2BmmWzWmXDdfx&download', 'images_4': 'https://www.cardiacatlas.org/share/download.php?id=101&token=Qn0dY2lHTyTIBF5qNHRkbHdUSnWnS4Yy&download', 'images_5': 'https://www.cardiacatlas.org/share/download.php?id=102&token=nYALJl6kS6cj5jgK9R8CKDpgQwZn1pm3&download', 'contours': 'https://www.cardiacatlas.org/share/download.php?id=61&token=gFjv8psvCQu2vDYldUW9TYUIDic11fDt&download', 'patient_data': 'https://www.cardiacatlas.org/share/download.php?id=66&token=Y1D66ieUdbXFlqmZ4icHJheJy44MXUPY&download'}
CHECKSUMS = {'images_1': '708ce04db1ac33948a00b9052d44e9548c6807121a4841f4c35080d6db127b72', 'images_2': 'ea97bd11dfc1154bdcf02b6466c00637bc7e0dc04b5ddc3d94d68a7cd7a3e6a5', 'images_3': '5b065fadef1b523bbabfec0d85327f375f743f2dd72810a5927b21b834b45deb', 'images_4': '2a8d7054d9b89b5af4c9764b4b04eecee57b61b71b69f5ac69ec12629e9a615c', 'images_5': 'e65a818fe89c665c344317222a052360e210936c81f89ceb9d36e8e3c6e2d6e9', 'contours': '93d8e06dfa53fc384e78810aeac86c336b5bdecbff1c9c175ec9254851cc2162', 'patient_data': 'c39eb11924d021e8ec51bb984daa57d1adadcc8eb7e342640181ce151dae5ded'}
LABEL_IDS = {'background': 0, 'cavity': 1, 'myocardium': 2}
INSTANCE_PATTERN = re.compile('IM-\\d+-(\\d+)-[io]contour-manual\\.txt$')
def get_scd_data(path: Union[os.PathLike, str], download: bool = False) -> str:
178def get_scd_data(path: Union[os.PathLike, str], download: bool = False) -> str:
179    """Download the SCD dataset.
180
181    Args:
182        path: Filepath to a folder where the data is downloaded for further processing.
183        download: Whether to download the data if it is not present.
184
185    Returns:
186        Filepath where the preprocessed data is stored.
187    """
188    preprocessed_dir = os.path.join(path, "preprocessed")
189    if glob(os.path.join(preprocessed_dir, "*_raw.tif")):
190        return preprocessed_dir
191
192    os.makedirs(path, exist_ok=True)
193
194    for key in ["images_1", "images_2", "images_3", "images_4", "images_5"]:
195        zip_path = os.path.join(path, f"{key}.zip")
196        util.download_source(
197            path=zip_path, url=URLS[key], download=download, checksum=CHECKSUMS[key], verify=False
198        )
199        util.unzip(zip_path=zip_path, dst=path)
200
201    contours_dir = os.path.join(path, "SCD_ManualContours")
202    if not os.path.exists(contours_dir):
203        zip_path = os.path.join(path, "contours.zip")
204        util.download_source(
205            path=zip_path, url=URLS["contours"], download=download, checksum=CHECKSUMS["contours"], verify=False
206        )
207        util.unzip(zip_path=zip_path, dst=path)
208
209    patient_csv_path = os.path.join(path, "scd_patientdata.csv")
210    util.download_source(
211        path=patient_csv_path,
212        url=URLS["patient_data"],
213        download=download,
214        checksum=CHECKSUMS["patient_data"],
215        verify=False,
216    )
217
218    _preprocess_inputs(path, preprocessed_dir)
219    return preprocessed_dir

Download the SCD 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_scd_paths( path: Union[os.PathLike, str], download: bool = False) -> Tuple[List[str], List[str]]:
222def get_scd_paths(path: Union[os.PathLike, str], download: bool = False) -> Tuple[List[str], List[str]]:
223    """Get paths to the SCD data.
224
225    Args:
226        path: Filepath to a folder where the data is downloaded for further processing.
227        download: Whether to download the data if it is not present.
228
229    Returns:
230        List of filepaths for the image data.
231        List of filepaths for the label data.
232    """
233    data_dir = get_scd_data(path, download)
234
235    gt_paths = natsorted(glob(os.path.join(data_dir, "*.tif")))
236    gt_paths = [p for p in gt_paths if not p.endswith("_raw.tif")]
237    image_paths = [p.replace(".tif", "_raw.tif") for p in gt_paths]
238
239    return image_paths, gt_paths

Get paths to the SCD data.

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:

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

def get_scd_dataset( path: Union[os.PathLike, str], patch_shape: Tuple[int, int], resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataset.Dataset:
242def get_scd_dataset(
243    path: Union[os.PathLike, str],
244    patch_shape: Tuple[int, int],
245    resize_inputs: bool = False,
246    download: bool = False,
247    **kwargs
248) -> Dataset:
249    """Get the SCD dataset for left ventricular cavity and myocardium segmentation.
250
251    Args:
252        path: Filepath to a folder where the data is downloaded for further processing.
253        patch_shape: The patch shape to use for training.
254        resize_inputs: Whether to resize the inputs to the expected patch shape.
255        download: Whether to download the data if it is not present.
256        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
257
258    Returns:
259        The segmentation dataset.
260    """
261    image_paths, gt_paths = get_scd_paths(path, download)
262
263    if resize_inputs:
264        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
265        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
266            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
267        )
268
269    return torch_em.default_segmentation_dataset(
270        raw_paths=image_paths,
271        raw_key=None,
272        label_paths=gt_paths,
273        label_key=None,
274        patch_shape=patch_shape,
275        is_seg_dataset=False,
276        **kwargs
277    )

Get the SCD dataset for left ventricular cavity and myocardium segmentation.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • patch_shape: The patch shape to use for training.
  • resize_inputs: Whether to resize the inputs to the expected 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_scd_loader( path: Union[os.PathLike, str], batch_size: int, patch_shape: Tuple[int, int], resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataloader.DataLoader:
280def get_scd_loader(
281    path: Union[os.PathLike, str],
282    batch_size: int,
283    patch_shape: Tuple[int, int],
284    resize_inputs: bool = False,
285    download: bool = False,
286    **kwargs
287) -> DataLoader:
288    """Get the SCD dataloader for left ventricular cavity and myocardium segmentation.
289
290    Args:
291        path: Filepath to a folder where the data is downloaded for further processing.
292        batch_size: The batch size for training.
293        patch_shape: The patch shape to use for training.
294        resize_inputs: Whether to resize the inputs to the expected patch shape.
295        download: Whether to download the data if it is not present.
296        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
297
298    Returns:
299        The DataLoader.
300    """
301    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
302    dataset = get_scd_dataset(path, patch_shape, resize_inputs, download, **ds_kwargs)
303    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)

Get the SCD dataloader for left ventricular cavity and myocardium 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.
  • resize_inputs: Whether to resize the inputs to the expected 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.