torch_em.data.datasets.medical.chaksu

The Chaksu dataset contains annotations for optic disc and optic cup segmentation in Fundus images.

This dataset is located at https://doi.org/10.6084/m9.figshare.20123135.v2, under the CC BY 4.0 license. The dataset is from the publication https://doi.org/10.1038/s41597-023-01943-4. Please cite it if you use this dataset for your research.

NOTE: The full archive also ships four other expert annotation and fusion variants at full uncompressed resolution, expanding to over 200GB. Only the raw fundus images and the STAPLE-fused consensus masks are downloaded here, as those are the ones needed for training.

  1"""The Chaksu dataset contains annotations for optic disc and optic cup
  2segmentation in Fundus images.
  3
  4This dataset is located at https://doi.org/10.6084/m9.figshare.20123135.v2, under the CC BY 4.0 license.
  5The dataset is from the publication https://doi.org/10.1038/s41597-023-01943-4.
  6Please cite it if you use this dataset for your research.
  7
  8NOTE: The full archive also ships four other expert annotation and fusion variants at full
  9uncompressed resolution, expanding to over 200GB. Only the raw fundus images and the
 10STAPLE-fused consensus masks are downloaded here, as those are the ones needed for training.
 11"""
 12
 13import io
 14import os
 15import zipfile
 16from glob import glob
 17from typing import Union, Tuple, Literal, List
 18
 19import requests
 20from tqdm import tqdm
 21import imageio.v3 as imageio
 22
 23from torch.utils.data import Dataset, DataLoader
 24
 25import torch_em
 26
 27from .. import util
 28
 29
 30URLS = {
 31    "train": "https://ndownloader.figshare.com/files/37875672",
 32    "test": "https://ndownloader.figshare.com/files/37875687",
 33}
 34DEVICES = ["Bosch", "Forus", "Remidio"]
 35
 36
 37class _RemoteZipFile(io.RawIOBase):
 38    """File-like wrapper that reads a remote zip archive via HTTP range requests."""
 39
 40    def __init__(self, url):
 41        self.url = url
 42        self.session = requests.Session()
 43        response = self.session.get(url, headers={"Range": "bytes=0-0"}, timeout=30)
 44        self.size = int(response.headers["Content-Range"].split("/")[-1])
 45        self.pos = 0
 46
 47    def readable(self):
 48        return True
 49
 50    def seekable(self):
 51        return True
 52
 53    def seek(self, offset, whence=0):
 54        if whence == 0:
 55            self.pos = offset
 56        elif whence == 1:
 57            self.pos += offset
 58        elif whence == 2:
 59            self.pos = self.size + offset
 60        return self.pos
 61
 62    def tell(self):
 63        return self.pos
 64
 65    def readinto(self, buffer):
 66        end = min(self.pos + len(buffer), self.size) - 1
 67        if end < self.pos:
 68            return 0
 69        response = self.session.get(self.url, headers={"Range": f"bytes={self.pos}-{end}"}, timeout=60)
 70        data = response.content
 71        buffer[:len(data)] = data
 72        self.pos += len(data)
 73        return len(data)
 74
 75
 76def _download_split(url, dst, split):
 77    remote_file = io.BufferedReader(_RemoteZipFile(url), buffer_size=1 << 20)
 78    with zipfile.ZipFile(remote_file) as zf:
 79        members = [
 80            name for name in zf.namelist() if not name.endswith("/") and "__MACOSX" not in name
 81            and not name.endswith(".DS_Store")
 82            and ("1.0_Original_Fundus_Images/" in name
 83                 or ("5.0_OD_OC_Mean_Median_Majority_STAPLE/" in name and "/STAPLE/" in name))
 84        ]
 85        for member in tqdm(members, desc=f"Downloading Chaksu '{split}' split"):
 86            zf.extract(member, dst)
 87
 88
 89def get_chaksu_data(path: Union[os.PathLike, str], download: bool = False) -> str:
 90    """Download the Chaksu dataset.
 91
 92    Args:
 93        path: Filepath to a folder where the data is downloaded for further processing.
 94        download: Whether to download the data if it is not present.
 95
 96    Returns:
 97        Filepath where the data is downloaded.
 98    """
 99    os.makedirs(path, exist_ok=True)
100    for split, url in URLS.items():
101        split_dir = os.path.join(path, split.capitalize())
102        if os.path.exists(split_dir):
103            continue
104        if not download:
105            raise RuntimeError(f"Chaksu data is not found at {split_dir} and 'download' is set to False.")
106        _download_split(url, path, split)
107
108    return path
109
110
111def _binarize_mask(gt_path, gt_dir):
112    dst_path = os.path.join(gt_dir, os.path.basename(gt_path))
113    if os.path.exists(dst_path):
114        return dst_path
115
116    os.makedirs(gt_dir, exist_ok=True)
117    mask = imageio.imread(gt_path)[..., 0] > 128  # the STAPLE masks are near-binary grayscale stored as RGBA
118    imageio.imwrite(dst_path, mask.astype("uint8"))
119    return dst_path
120
121
122def get_chaksu_paths(
123    path: Union[os.PathLike, str],
124    split: Literal['train', 'test'],
125    task: Literal["optic_disc", "optic_cup"] = "optic_disc",
126    download: bool = False,
127) -> Tuple[List[str], List[str]]:
128    """Get paths to the Chaksu data.
129
130    Args:
131        path: Filepath to a folder where the data is downloaded for further processing.
132        split: The choice of data split.
133        task: The choice of labels for the specific task.
134        download: Whether to download the data if it is not present.
135
136    Returns:
137        List of filepaths for the image data.
138        List of filepaths for the label data.
139    """
140    data_dir = get_chaksu_data(path=path, download=download)
141
142    assert split in ["train", "test"], f"'{split}' is not a valid split."
143    assert task in ["optic_disc", "optic_cup"], f"'{task}' is not a valid task."
144
145    split_dir = split.capitalize()
146    region = "Disc" if task == "optic_disc" else "Cup"
147
148    image_paths, gt_paths = [], []
149    for device in DEVICES:
150        device_image_paths = sorted(glob(os.path.join(data_dir, split_dir, "1.0_Original_Fundus_Images", device, "*")))
151        gt_dir = os.path.join(data_dir, split_dir, "segmentation_masks", device, region)
152        for image_path in device_image_paths:
153            stem = os.path.splitext(os.path.basename(image_path))[0]
154            staple_path = os.path.join(
155                data_dir, split_dir, "5.0_OD_OC_Mean_Median_Majority_STAPLE", device, region, "STAPLE", f"{stem}.png"
156            )
157            if not os.path.exists(staple_path):
158                continue
159
160            image_paths.append(image_path)
161            gt_paths.append(_binarize_mask(staple_path, gt_dir))
162
163    assert len(image_paths) == len(gt_paths) and len(image_paths) > 0
164
165    return image_paths, gt_paths
166
167
168def get_chaksu_dataset(
169    path: Union[os.PathLike, str],
170    patch_shape: Tuple[int, int],
171    split: Literal['train', 'test'],
172    task: Literal["optic_disc", "optic_cup"] = "optic_disc",
173    resize_inputs: bool = False,
174    download: bool = False,
175    **kwargs
176) -> Dataset:
177    """Get the Chaksu dataset for segmentation of optic disc and optic cup in fundus images.
178
179    Args:
180        path: Filepath to a folder where the data is downloaded for further processing.
181        patch_shape: The patch shape to use for training.
182        split: The choice of data split.
183        task: The choice of labels for the specific task.
184        resize_inputs: Whether to resize the inputs to the expected patch shape.
185        download: Whether to download the data if it is not present.
186        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
187
188    Returns:
189        The segmentation dataset.
190    """
191    image_paths, gt_paths = get_chaksu_paths(path, split, task, download)
192
193    if resize_inputs:
194        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": True}
195        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
196            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
197        )
198
199    return torch_em.default_segmentation_dataset(
200        raw_paths=image_paths,
201        raw_key=None,
202        label_paths=gt_paths,
203        label_key=None,
204        patch_shape=patch_shape,
205        is_seg_dataset=False,
206        **kwargs
207    )
208
209
210def get_chaksu_loader(
211    path: Union[os.PathLike, str],
212    batch_size: int,
213    patch_shape: Tuple[int, int],
214    split: Literal['train', 'test'],
215    task: Literal["optic_disc", "optic_cup"] = "optic_disc",
216    resize_inputs: bool = False,
217    download: bool = False,
218    **kwargs
219) -> DataLoader:
220    """Get the Chaksu dataloader for segmentation of optic disc and optic cup in fundus images.
221
222    Args:
223        path: Filepath to a folder where the data is downloaded for further processing.
224        batch_size: The batch size for training.
225        patch_shape: The patch shape to use for training.
226        split: The choice of data split.
227        task: The choice of labels for the specific task.
228        resize_inputs: Whether to resize the inputs to the expected patch shape.
229        download: Whether to download the data if it is not present.
230        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
231
232    Returns:
233        The DataLoader.
234    """
235    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
236    dataset = get_chaksu_dataset(path, patch_shape, split, task, resize_inputs, download, **ds_kwargs)
237    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)
URLS = {'train': 'https://ndownloader.figshare.com/files/37875672', 'test': 'https://ndownloader.figshare.com/files/37875687'}
DEVICES = ['Bosch', 'Forus', 'Remidio']
def get_chaksu_data(path: Union[os.PathLike, str], download: bool = False) -> str:
 90def get_chaksu_data(path: Union[os.PathLike, str], download: bool = False) -> str:
 91    """Download the Chaksu dataset.
 92
 93    Args:
 94        path: Filepath to a folder where the data is downloaded for further processing.
 95        download: Whether to download the data if it is not present.
 96
 97    Returns:
 98        Filepath where the data is downloaded.
 99    """
100    os.makedirs(path, exist_ok=True)
101    for split, url in URLS.items():
102        split_dir = os.path.join(path, split.capitalize())
103        if os.path.exists(split_dir):
104            continue
105        if not download:
106            raise RuntimeError(f"Chaksu data is not found at {split_dir} and 'download' is set to False.")
107        _download_split(url, path, split)
108
109    return path

Download the Chaksu 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_chaksu_paths( path: Union[os.PathLike, str], split: Literal['train', 'test'], task: Literal['optic_disc', 'optic_cup'] = 'optic_disc', download: bool = False) -> Tuple[List[str], List[str]]:
123def get_chaksu_paths(
124    path: Union[os.PathLike, str],
125    split: Literal['train', 'test'],
126    task: Literal["optic_disc", "optic_cup"] = "optic_disc",
127    download: bool = False,
128) -> Tuple[List[str], List[str]]:
129    """Get paths to the Chaksu data.
130
131    Args:
132        path: Filepath to a folder where the data is downloaded for further processing.
133        split: The choice of data split.
134        task: The choice of labels for the specific task.
135        download: Whether to download the data if it is not present.
136
137    Returns:
138        List of filepaths for the image data.
139        List of filepaths for the label data.
140    """
141    data_dir = get_chaksu_data(path=path, download=download)
142
143    assert split in ["train", "test"], f"'{split}' is not a valid split."
144    assert task in ["optic_disc", "optic_cup"], f"'{task}' is not a valid task."
145
146    split_dir = split.capitalize()
147    region = "Disc" if task == "optic_disc" else "Cup"
148
149    image_paths, gt_paths = [], []
150    for device in DEVICES:
151        device_image_paths = sorted(glob(os.path.join(data_dir, split_dir, "1.0_Original_Fundus_Images", device, "*")))
152        gt_dir = os.path.join(data_dir, split_dir, "segmentation_masks", device, region)
153        for image_path in device_image_paths:
154            stem = os.path.splitext(os.path.basename(image_path))[0]
155            staple_path = os.path.join(
156                data_dir, split_dir, "5.0_OD_OC_Mean_Median_Majority_STAPLE", device, region, "STAPLE", f"{stem}.png"
157            )
158            if not os.path.exists(staple_path):
159                continue
160
161            image_paths.append(image_path)
162            gt_paths.append(_binarize_mask(staple_path, gt_dir))
163
164    assert len(image_paths) == len(gt_paths) and len(image_paths) > 0
165
166    return image_paths, gt_paths

Get paths to the Chaksu data.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • split: The choice of data split.
  • task: The choice of labels for the specific task.
  • 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_chaksu_dataset( path: Union[os.PathLike, str], patch_shape: Tuple[int, int], split: Literal['train', 'test'], task: Literal['optic_disc', 'optic_cup'] = 'optic_disc', resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataset.Dataset:
169def get_chaksu_dataset(
170    path: Union[os.PathLike, str],
171    patch_shape: Tuple[int, int],
172    split: Literal['train', 'test'],
173    task: Literal["optic_disc", "optic_cup"] = "optic_disc",
174    resize_inputs: bool = False,
175    download: bool = False,
176    **kwargs
177) -> Dataset:
178    """Get the Chaksu dataset for segmentation of optic disc and optic cup in fundus images.
179
180    Args:
181        path: Filepath to a folder where the data is downloaded for further processing.
182        patch_shape: The patch shape to use for training.
183        split: The choice of data split.
184        task: The choice of labels for the specific task.
185        resize_inputs: Whether to resize the inputs to the expected patch shape.
186        download: Whether to download the data if it is not present.
187        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
188
189    Returns:
190        The segmentation dataset.
191    """
192    image_paths, gt_paths = get_chaksu_paths(path, split, task, download)
193
194    if resize_inputs:
195        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": True}
196        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
197            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
198        )
199
200    return torch_em.default_segmentation_dataset(
201        raw_paths=image_paths,
202        raw_key=None,
203        label_paths=gt_paths,
204        label_key=None,
205        patch_shape=patch_shape,
206        is_seg_dataset=False,
207        **kwargs
208    )

Get the Chaksu dataset for segmentation of optic disc and optic cup in fundus images.

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.
  • task: The choice of labels for the specific task.
  • 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_chaksu_loader( path: Union[os.PathLike, str], batch_size: int, patch_shape: Tuple[int, int], split: Literal['train', 'test'], task: Literal['optic_disc', 'optic_cup'] = 'optic_disc', resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataloader.DataLoader:
211def get_chaksu_loader(
212    path: Union[os.PathLike, str],
213    batch_size: int,
214    patch_shape: Tuple[int, int],
215    split: Literal['train', 'test'],
216    task: Literal["optic_disc", "optic_cup"] = "optic_disc",
217    resize_inputs: bool = False,
218    download: bool = False,
219    **kwargs
220) -> DataLoader:
221    """Get the Chaksu dataloader for segmentation of optic disc and optic cup in fundus images.
222
223    Args:
224        path: Filepath to a folder where the data is downloaded for further processing.
225        batch_size: The batch size for training.
226        patch_shape: The patch shape to use for training.
227        split: The choice of data split.
228        task: The choice of labels for the specific task.
229        resize_inputs: Whether to resize the inputs to the expected patch shape.
230        download: Whether to download the data if it is not present.
231        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
232
233    Returns:
234        The DataLoader.
235    """
236    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
237    dataset = get_chaksu_dataset(path, patch_shape, split, task, resize_inputs, download, **ds_kwargs)
238    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)

Get the Chaksu dataloader for segmentation of optic disc and optic cup in fundus images.

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.
  • task: The choice of labels for the specific task.
  • 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.