torch_em.data.datasets.medical.echonet_dynamic

EchoNet-Dynamic contains annotations for left ventricle segmentation in apical-4-chamber echocardiography videos, together with cardiac function labels (ejection fraction, end-systolic and end-diastolic volume).

The dataset comprises 10,030 videos, collected at Stanford Health Care. Each video only has expert tracings for the two labeled frames (end-diastole and end-systole) used to compute the left ventricular ejection fraction, not dense per-frame masks. The dataset is located at https://echonet.github.io/dynamic/ and distributed through the Stanford AIMI Center Shared Datasets Portal under a non-commercial research use agreement: registration as an individual user is required, re-distribution (including sharing the download link) is forbidden, and re-identification attempts are prohibited. This module cannot download the dataset automatically; see get_echonet_dynamic_data for the manual steps.

This dataset is from the publication https://doi.org/10.1038/s41586-020-2145-8 (cite the DOI https://doi.org/10.71718/yqp5-y078 for the data itself). Please cite them if you use this dataset in your research.

NOTE: Reading the videos requires 'opencv-python' ('cv2'), and the tracing rasterization requires 'scikit-image'.

  1"""EchoNet-Dynamic contains annotations for left ventricle segmentation in apical-4-chamber
  2echocardiography videos, together with cardiac function labels (ejection fraction, end-systolic
  3and end-diastolic volume).
  4
  5The dataset comprises 10,030 videos, collected at Stanford Health Care. Each video only has expert
  6tracings for the two labeled frames (end-diastole and end-systole) used to compute the left
  7ventricular ejection fraction, not dense per-frame masks. The dataset is located at
  8https://echonet.github.io/dynamic/ and distributed through the Stanford AIMI Center Shared Datasets
  9Portal under a non-commercial research use agreement: registration as an individual user is
 10required, re-distribution (including sharing the download link) is forbidden, and re-identification
 11attempts are prohibited. This module cannot download the dataset automatically; see
 12`get_echonet_dynamic_data` for the manual steps.
 13
 14This dataset is from the publication https://doi.org/10.1038/s41586-020-2145-8 (cite the DOI
 15https://doi.org/10.71718/yqp5-y078 for the data itself). Please cite them if you use this dataset in
 16your research.
 17
 18NOTE: Reading the videos requires 'opencv-python' ('cv2'), and the tracing rasterization requires
 19'scikit-image'.
 20"""
 21
 22import os
 23from glob import glob
 24from tqdm import tqdm
 25from natsort import natsorted
 26from typing import Union, Tuple, Literal, List
 27
 28import numpy as np
 29import pandas as pd
 30import imageio.v3 as imageio
 31
 32from torch.utils.data import Dataset, DataLoader
 33
 34import torch_em
 35
 36from .. import util
 37
 38
 39SPLITS = ("TRAIN", "VAL", "TEST")
 40
 41
 42def _trace_to_mask(x1, y1, x2, y2, shape):
 43    from skimage.draw import polygon
 44
 45    # Follows the rasterization used by the EchoNet dataset releases: the first coordinate pair is
 46    # the long axis of the left ventricle, and the remaining pairs are the perpendicular short-axis
 47    # distances. Walking the two sides of the short-axis points (one side forwards, the other
 48    # backwards) traces the outline of the traced region, which is then filled in.
 49    x = np.concatenate((x1[1:], x2[1:][::-1]))
 50    y = np.concatenate((y1[1:], y2[1:][::-1]))
 51
 52    rows, cols = polygon(np.round(y).astype(int), np.round(x).astype(int), shape=shape)
 53    mask = np.zeros(shape, dtype="uint8")
 54    mask[rows, cols] = 1
 55    return mask
 56
 57
 58def _find_video_path(videos_dir, file_name):
 59    stem = os.path.splitext(file_name)[0]
 60    for candidate in (file_name, f"{stem}.avi", f"{stem}.mp4"):
 61        candidate_path = os.path.join(videos_dir, candidate)
 62        if os.path.exists(candidate_path):
 63            return candidate_path
 64    return None
 65
 66
 67def _preprocess_inputs(data_dir, preprocessed_dir):
 68    import cv2
 69
 70    file_list = pd.read_csv(os.path.join(data_dir, "FileList.csv"))
 71    tracings = pd.read_csv(os.path.join(data_dir, "VolumeTracings.csv"))
 72
 73    videos_dir = os.path.join(data_dir, "Videos")
 74    os.makedirs(preprocessed_dir, exist_ok=True)
 75
 76    image_paths, gt_paths = [], []
 77    for _, row in tqdm(file_list.iterrows(), total=len(file_list), desc="Preprocess EchoNet-Dynamic"):
 78        file_name = str(row["FileName"])
 79        stem = os.path.splitext(file_name)[0]
 80        split = str(row["Split"]).upper()
 81
 82        video_tracings = tracings[tracings["FileName"] == file_name]
 83        if video_tracings.empty:
 84            video_tracings = tracings[tracings["FileName"] == stem + ".avi"]
 85        if video_tracings.empty:
 86            continue
 87
 88        video_path = _find_video_path(videos_dir, file_name)
 89        if video_path is None:
 90            continue
 91
 92        capture = cv2.VideoCapture(video_path)
 93
 94        for frame_idx, frame_tracings in video_tracings.groupby("Frame"):
 95            image_path = os.path.join(preprocessed_dir, f"{stem}_{split}_{frame_idx}.tif")
 96            mask_path = os.path.join(preprocessed_dir, f"{stem}_{split}_{frame_idx}_mask.tif")
 97
 98            if os.path.exists(image_path) and os.path.exists(mask_path):
 99                image_paths.append(image_path)
100                gt_paths.append(mask_path)
101                continue
102
103            capture.set(cv2.CAP_PROP_POS_FRAMES, int(frame_idx))
104            success, frame = capture.read()
105            if not success:
106                continue
107
108            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
109            mask = _trace_to_mask(
110                frame_tracings["X1"].to_numpy(),
111                frame_tracings["Y1"].to_numpy(),
112                frame_tracings["X2"].to_numpy(),
113                frame_tracings["Y2"].to_numpy(),
114                frame.shape,
115            )
116
117            imageio.imwrite(image_path, frame)
118            imageio.imwrite(mask_path, mask)
119
120            image_paths.append(image_path)
121            gt_paths.append(mask_path)
122
123        capture.release()
124
125    return image_paths, gt_paths
126
127
128def get_echonet_dynamic_data(path: Union[os.PathLike, str]) -> str:
129    """Obtain the EchoNet-Dynamic dataset.
130
131    NOTE: 'torch_em' cannot download this dataset, as it requires individual registration and
132    agreement to the EchoNet-Dynamic Research Use Agreement. Please follow these steps:
133    - Visit https://echonet.github.io/dynamic/ and follow the access instructions.
134    - Register (individually, per user) with the Stanford AIMI Center Shared Datasets Portal
135      (https://stanford.redivis.com/datasets/66s1-2hsmzj5rn) and agree to the Research Use Agreement
136      (non-commercial research use only, no re-distribution).
137    - Download the dataset and place it at `path`, so that it has the following structure:
138      `path/Videos`, `path/FileList.csv`, and `path/VolumeTracings.csv`.
139
140    Args:
141        path: Filepath to a folder where the dataset is stored.
142
143    Returns:
144        Filepath to the folder where the dataset is stored.
145    """
146    if not (
147        os.path.exists(os.path.join(path, "FileList.csv"))
148        and os.path.exists(os.path.join(path, "VolumeTracings.csv"))
149        and os.path.exists(os.path.join(path, "Videos"))
150    ):
151        raise RuntimeError(
152            f"Cannot find the EchoNet-Dynamic data at '{path}'. "
153            "This dataset requires manual download, see `get_echonet_dynamic_data` for the steps."
154        )
155
156    return path
157
158
159def get_echonet_dynamic_paths(
160    path: Union[os.PathLike, str], split: Literal["TRAIN", "VAL", "TEST", None] = None,
161) -> Tuple[List[str], List[str]]:
162    """Get paths to the EchoNet-Dynamic data.
163
164    Args:
165        path: Filepath to a folder where the dataset is stored.
166        split: The choice of data split. Either 'TRAIN', 'VAL' or 'TEST'. By default, all splits are used.
167
168    Returns:
169        List of filepaths for the image data.
170        List of filepaths for the label data.
171    """
172    data_dir = get_echonet_dynamic_data(path)
173
174    preprocessed_dir = os.path.join(path, "preprocessed")
175
176    image_paths = natsorted(glob(os.path.join(preprocessed_dir, "*.tif")))
177    image_paths = [p for p in image_paths if not p.endswith("_mask.tif")]
178    if not image_paths:
179        image_paths, _ = _preprocess_inputs(data_dir, preprocessed_dir)
180        image_paths = natsorted(image_paths)
181
182    gt_paths = natsorted([p.replace(".tif", "_mask.tif") for p in image_paths])
183    assert len(image_paths) == len(gt_paths) and len(image_paths) > 0
184
185    if split is not None:
186        assert split in SPLITS, f"'{split}' is not a valid split choice for the EchoNet-Dynamic dataset."
187        image_paths = [p for p in image_paths if f"_{split}_" in os.path.basename(p)]
188        gt_paths = [p for p in gt_paths if f"_{split}_" in os.path.basename(p)]
189
190    return image_paths, gt_paths
191
192
193def get_echonet_dynamic_dataset(
194    path: Union[os.PathLike, str],
195    patch_shape: Tuple[int, int],
196    split: Literal["TRAIN", "VAL", "TEST", None] = None,
197    resize_inputs: bool = False,
198    **kwargs
199) -> Dataset:
200    """Get the EchoNet-Dynamic dataset for left ventricle segmentation in echocardiography videos.
201
202    Args:
203        path: Filepath to a folder where the dataset is stored.
204        patch_shape: The patch shape to use for training.
205        split: The choice of data split. Either 'TRAIN', 'VAL' or 'TEST'. By default, all splits are used.
206        resize_inputs: Whether to resize inputs to the desired patch shape.
207        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
208
209    Returns:
210        The segmentation dataset.
211    """
212    image_paths, gt_paths = get_echonet_dynamic_paths(path, split)
213
214    if resize_inputs:
215        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
216        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
217            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
218        )
219
220    return torch_em.default_segmentation_dataset(
221        raw_paths=image_paths,
222        raw_key=None,
223        label_paths=gt_paths,
224        label_key=None,
225        patch_shape=patch_shape,
226        is_seg_dataset=False,
227        **kwargs
228    )
229
230
231def get_echonet_dynamic_loader(
232    path: Union[os.PathLike, str],
233    batch_size: int,
234    patch_shape: Tuple[int, int],
235    split: Literal["TRAIN", "VAL", "TEST", None] = None,
236    resize_inputs: bool = False,
237    **kwargs
238) -> DataLoader:
239    """Get the EchoNet-Dynamic dataloader for left ventricle segmentation in echocardiography videos.
240
241    Args:
242        path: Filepath to a folder where the dataset is stored.
243        batch_size: The batch size for training.
244        patch_shape: The patch shape to use for training.
245        split: The choice of data split. Either 'TRAIN', 'VAL' or 'TEST'. By default, all splits are used.
246        resize_inputs: Whether to resize inputs to the desired patch shape.
247        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
248
249    Returns:
250        The DataLoader.
251    """
252    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
253    dataset = get_echonet_dynamic_dataset(path, patch_shape, split, resize_inputs, **ds_kwargs)
254    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)
SPLITS = ('TRAIN', 'VAL', 'TEST')
def get_echonet_dynamic_data(path: Union[os.PathLike, str]) -> str:
129def get_echonet_dynamic_data(path: Union[os.PathLike, str]) -> str:
130    """Obtain the EchoNet-Dynamic dataset.
131
132    NOTE: 'torch_em' cannot download this dataset, as it requires individual registration and
133    agreement to the EchoNet-Dynamic Research Use Agreement. Please follow these steps:
134    - Visit https://echonet.github.io/dynamic/ and follow the access instructions.
135    - Register (individually, per user) with the Stanford AIMI Center Shared Datasets Portal
136      (https://stanford.redivis.com/datasets/66s1-2hsmzj5rn) and agree to the Research Use Agreement
137      (non-commercial research use only, no re-distribution).
138    - Download the dataset and place it at `path`, so that it has the following structure:
139      `path/Videos`, `path/FileList.csv`, and `path/VolumeTracings.csv`.
140
141    Args:
142        path: Filepath to a folder where the dataset is stored.
143
144    Returns:
145        Filepath to the folder where the dataset is stored.
146    """
147    if not (
148        os.path.exists(os.path.join(path, "FileList.csv"))
149        and os.path.exists(os.path.join(path, "VolumeTracings.csv"))
150        and os.path.exists(os.path.join(path, "Videos"))
151    ):
152        raise RuntimeError(
153            f"Cannot find the EchoNet-Dynamic data at '{path}'. "
154            "This dataset requires manual download, see `get_echonet_dynamic_data` for the steps."
155        )
156
157    return path

Obtain the EchoNet-Dynamic dataset.

NOTE: 'torch_em' cannot download this dataset, as it requires individual registration and agreement to the EchoNet-Dynamic Research Use Agreement. Please follow these steps:

  • Visit https://echonet.github.io/dynamic/ and follow the access instructions.
  • Register (individually, per user) with the Stanford AIMI Center Shared Datasets Portal (https://stanford.redivis.com/datasets/66s1-2hsmzj5rn) and agree to the Research Use Agreement (non-commercial research use only, no re-distribution).
  • Download the dataset and place it at path, so that it has the following structure: path/Videos, path/FileList.csv, and path/VolumeTracings.csv.
Arguments:
  • path: Filepath to a folder where the dataset is stored.
Returns:

Filepath to the folder where the dataset is stored.

def get_echonet_dynamic_paths( path: Union[os.PathLike, str], split: Literal['TRAIN', 'VAL', 'TEST', None] = None) -> Tuple[List[str], List[str]]:
160def get_echonet_dynamic_paths(
161    path: Union[os.PathLike, str], split: Literal["TRAIN", "VAL", "TEST", None] = None,
162) -> Tuple[List[str], List[str]]:
163    """Get paths to the EchoNet-Dynamic data.
164
165    Args:
166        path: Filepath to a folder where the dataset is stored.
167        split: The choice of data split. Either 'TRAIN', 'VAL' or 'TEST'. By default, all splits are used.
168
169    Returns:
170        List of filepaths for the image data.
171        List of filepaths for the label data.
172    """
173    data_dir = get_echonet_dynamic_data(path)
174
175    preprocessed_dir = os.path.join(path, "preprocessed")
176
177    image_paths = natsorted(glob(os.path.join(preprocessed_dir, "*.tif")))
178    image_paths = [p for p in image_paths if not p.endswith("_mask.tif")]
179    if not image_paths:
180        image_paths, _ = _preprocess_inputs(data_dir, preprocessed_dir)
181        image_paths = natsorted(image_paths)
182
183    gt_paths = natsorted([p.replace(".tif", "_mask.tif") for p in image_paths])
184    assert len(image_paths) == len(gt_paths) and len(image_paths) > 0
185
186    if split is not None:
187        assert split in SPLITS, f"'{split}' is not a valid split choice for the EchoNet-Dynamic dataset."
188        image_paths = [p for p in image_paths if f"_{split}_" in os.path.basename(p)]
189        gt_paths = [p for p in gt_paths if f"_{split}_" in os.path.basename(p)]
190
191    return image_paths, gt_paths

Get paths to the EchoNet-Dynamic data.

Arguments:
  • path: Filepath to a folder where the dataset is stored.
  • split: The choice of data split. Either 'TRAIN', 'VAL' or 'TEST'. By default, all splits are used.
Returns:

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

def get_echonet_dynamic_dataset( path: Union[os.PathLike, str], patch_shape: Tuple[int, int], split: Literal['TRAIN', 'VAL', 'TEST', None] = None, resize_inputs: bool = False, **kwargs) -> torch.utils.data.dataset.Dataset:
194def get_echonet_dynamic_dataset(
195    path: Union[os.PathLike, str],
196    patch_shape: Tuple[int, int],
197    split: Literal["TRAIN", "VAL", "TEST", None] = None,
198    resize_inputs: bool = False,
199    **kwargs
200) -> Dataset:
201    """Get the EchoNet-Dynamic dataset for left ventricle segmentation in echocardiography videos.
202
203    Args:
204        path: Filepath to a folder where the dataset is stored.
205        patch_shape: The patch shape to use for training.
206        split: The choice of data split. Either 'TRAIN', 'VAL' or 'TEST'. By default, all splits are used.
207        resize_inputs: Whether to resize inputs to the desired patch shape.
208        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
209
210    Returns:
211        The segmentation dataset.
212    """
213    image_paths, gt_paths = get_echonet_dynamic_paths(path, split)
214
215    if resize_inputs:
216        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
217        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
218            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
219        )
220
221    return torch_em.default_segmentation_dataset(
222        raw_paths=image_paths,
223        raw_key=None,
224        label_paths=gt_paths,
225        label_key=None,
226        patch_shape=patch_shape,
227        is_seg_dataset=False,
228        **kwargs
229    )

Get the EchoNet-Dynamic dataset for left ventricle segmentation in echocardiography videos.

Arguments:
  • path: Filepath to a folder where the dataset is stored.
  • patch_shape: The patch shape to use for training.
  • split: The choice of data split. Either 'TRAIN', 'VAL' or 'TEST'. By default, all splits are used.
  • resize_inputs: Whether to resize inputs to the desired patch shape.
  • kwargs: Additional keyword arguments for torch_em.default_segmentation_dataset.
Returns:

The segmentation dataset.

def get_echonet_dynamic_loader( path: Union[os.PathLike, str], batch_size: int, patch_shape: Tuple[int, int], split: Literal['TRAIN', 'VAL', 'TEST', None] = None, resize_inputs: bool = False, **kwargs) -> torch.utils.data.dataloader.DataLoader:
232def get_echonet_dynamic_loader(
233    path: Union[os.PathLike, str],
234    batch_size: int,
235    patch_shape: Tuple[int, int],
236    split: Literal["TRAIN", "VAL", "TEST", None] = None,
237    resize_inputs: bool = False,
238    **kwargs
239) -> DataLoader:
240    """Get the EchoNet-Dynamic dataloader for left ventricle segmentation in echocardiography videos.
241
242    Args:
243        path: Filepath to a folder where the dataset is stored.
244        batch_size: The batch size for training.
245        patch_shape: The patch shape to use for training.
246        split: The choice of data split. Either 'TRAIN', 'VAL' or 'TEST'. By default, all splits are used.
247        resize_inputs: Whether to resize inputs to the desired patch shape.
248        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
249
250    Returns:
251        The DataLoader.
252    """
253    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
254    dataset = get_echonet_dynamic_dataset(path, patch_shape, split, resize_inputs, **ds_kwargs)
255    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)

Get the EchoNet-Dynamic dataloader for left ventricle segmentation in echocardiography videos.

Arguments:
  • path: Filepath to a folder where the dataset is stored.
  • batch_size: The batch size for training.
  • patch_shape: The patch shape to use for training.
  • split: The choice of data split. Either 'TRAIN', 'VAL' or 'TEST'. By default, all splits are used.
  • resize_inputs: Whether to resize inputs to the desired patch shape.
  • kwargs: Additional keyword arguments for torch_em.default_segmentation_dataset or for the PyTorch DataLoader.
Returns:

The DataLoader.