torch_em.data.datasets.light_microscopy.svia

The SVIA dataset contains annotations for sperm segmentation in bright-field microscopy videos of human semen.

SVIA stands for Sperm Videos and Images Analysis. The deposit calls the dataset MIaMIA-SVDS. This loader uses Subset-B, which holds a mask per sperm for 451 frames of ten videos, so 25966 objects.

NOTE: The dataset holds two more label types, which this loader does not return.

  • Subset-A holds a bounding box and a class for 125880 objects of 3622 frames. The classes are 'S' for a sperm and 'Impurity' for everything else, at a ratio of about 27 to 1. Subset-C cuts one small image per box out of the frames, for a classification task. Read 'Subset-A/.xml' and 'Subset-C/V
  • The masks of Subset-B carry a track id, because the file name of a mask ends with the id of the sperm and that id follows the same sperm over the frames of a video. This loader keeps the id as the label value, so the labels of one video track the sperm through time.

NOTE: Only 10 of the 101 videos carry masks. The other 91 videos hold boxes alone.

NOTE: The archive is a rar file, so the extraction needs the 'rarfile' package and the 'unrar' program. The masks are stored as one full frame image per sperm, which is why the archive holds 25966 mask files for 451 frames.

NOTE: The publication defines no split. This loader splits by video, because the frames of one video show the same sperm over time and a split over frames would leak.

The dataset is located at https://doi.org/10.6084/m9.figshare.15074253.v1. This dataset is from the publication https://doi.org/10.1016/j.bbe.2021.12.010. Please cite it if you use this dataset in your research. The authors welcome non-commercial research work on this data.

  1"""The SVIA dataset contains annotations for sperm segmentation in
  2bright-field microscopy videos of human semen.
  3
  4SVIA stands for Sperm Videos and Images Analysis. The deposit calls the dataset MIaMIA-SVDS. This
  5loader uses Subset-B, which holds a mask per sperm for 451 frames of ten videos, so 25966 objects.
  6
  7NOTE: The dataset holds two more label types, which this loader does not return.
  8- Subset-A holds a bounding box and a class for 125880 objects of 3622 frames. The classes are
  9  'S' for a sperm and 'Impurity' for everything else, at a ratio of about 27 to 1. Subset-C cuts one
 10  small image per box out of the frames, for a classification task. Read 'Subset-A/<frame>.xml' and
 11  'Subset-C/V<video>-F<frame>-<class><index>.png' if you need them.
 12- The masks of Subset-B carry a track id, because the file name of a mask ends with the id of the
 13  sperm and that id follows the same sperm over the frames of a video. This loader keeps the id as
 14  the label value, so the labels of one video track the sperm through time.
 15
 16NOTE: Only 10 of the 101 videos carry masks. The other 91 videos hold boxes alone.
 17
 18NOTE: The archive is a rar file, so the extraction needs the 'rarfile' package and the 'unrar'
 19program. The masks are stored as one full frame image per sperm, which is why the archive holds
 2025966 mask files for 451 frames.
 21
 22NOTE: The publication defines no split. This loader splits by video, because the frames of one video
 23show the same sperm over time and a split over frames would leak.
 24
 25The dataset is located at https://doi.org/10.6084/m9.figshare.15074253.v1.
 26This dataset is from the publication https://doi.org/10.1016/j.bbe.2021.12.010.
 27Please cite it if you use this dataset in your research. The authors welcome non-commercial research
 28work on this data.
 29"""
 30
 31import os
 32import re
 33from glob import glob
 34from pathlib import Path
 35from natsort import natsorted
 36from typing import List, Literal, Optional, Sequence, Tuple, Union
 37
 38import numpy as np
 39import imageio.v3 as imageio
 40
 41from torch.utils.data import DataLoader, Dataset
 42
 43import torch_em
 44
 45from .. import util
 46
 47
 48URL = "https://ndownloader.figshare.com/files/28986378"
 49CHECKSUM = "f6956e21eab440806f76a9805655b8a7e56f2555e8f6a03983d2eebab4b2ba71"
 50
 51ARCHIVE_ROOT = "Data Set"
 52
 53# The ten videos of Subset-B, and how many annotated frames each of them holds.
 54VIDEOS = {
 55    "S_0001": 45, "S_0003": 38, "S_0005": 36, "S_0006": 26, "S_0007": 10,
 56    "S_0008": 90, "S_0009": 48, "S_0010": 32, "S_0011": 39, "S_0012": 87,
 57}
 58
 59# A video belongs to one split only, so the frames of a video never spread over two splits.
 60SPLITS = {
 61    "train": ("S_0001", "S_0005", "S_0008", "S_0010", "S_0011"),  # 242 frames
 62    "val": ("S_0003", "S_0007"),  # 48 frames
 63    "test": ("S_0006", "S_0009", "S_0012"),  # 161 frames
 64}
 65
 66# The masks are not clean binary images, so the loader compares against the middle of the range.
 67MASK_THRESHOLD = 127
 68
 69
 70def _extract_archive(archive_path: str, path: str) -> None:
 71    """Extract the frames and the masks of Subset-B out of the rar archive."""
 72    try:
 73        import rarfile
 74    except ImportError:
 75        raise RuntimeError(
 76            "The 'rarfile' package is required to extract the SVIA archive. "
 77            "Install it with 'pip install rarfile', and install the 'unrar' program as well."
 78        )
 79
 80    with rarfile.RarFile(archive_path) as archive:
 81        members = [
 82            name for name in archive.namelist()
 83            if name.startswith(f"{ARCHIVE_ROOT}/Subset-B/")
 84            or name.startswith(f"{ARCHIVE_ROOT}/Frames from original videos/")
 85        ]
 86        if not members:
 87            raise RuntimeError(f"The archive {archive_path} does not hold Subset-B.")
 88        archive.extractall(path, members=members)
 89
 90
 91def _get_track_id(mask_stem: str, frame_name: str) -> int:
 92    """Read the track id of a sperm out of the name of its mask.
 93
 94    A mask of the frame 'S_0001_0026' is normally named 'S_0001_0026_0010'. Two files of the
 95    archive break that rule, 'S_0001_0026-0010' and 'S_0003_001311png', so the id comes from the
 96    part of the name that follows the name of the frame.
 97    """
 98    remainder = mask_stem[len(frame_name):] if mask_stem.startswith(frame_name) else mask_stem
 99    digits = re.findall(r"\d+", remainder)
100    if not digits:
101        raise RuntimeError(f"Could not read a track id from the mask '{mask_stem}'.")
102    return int(digits[0])
103
104
105def _create_instance_labels(data_dir: str, video: str) -> str:
106    """Merge the per sperm masks of every frame into one instance label image."""
107    from tqdm import tqdm
108
109    label_dir = os.path.join(data_dir, "instance_labels", video)
110    os.makedirs(label_dir, exist_ok=True)
111
112    frame_dirs = natsorted(glob(os.path.join(data_dir, "Subset-B", video, "*")))
113    for frame_dir in tqdm(frame_dirs, desc=f"Preprocess '{video}'"):
114        name = os.path.basename(frame_dir)
115        output_path = os.path.join(label_dir, f"{name}.tif")
116        if os.path.exists(output_path):
117            continue
118
119        image_path = os.path.join(data_dir, "Frames from original videos", video, f"{name}.png")
120        if not os.path.exists(image_path):
121            continue
122
123        shape = imageio.imread(image_path).shape[:2]
124        labels = np.zeros(shape, dtype="uint16")
125        for mask_path in natsorted(glob(os.path.join(frame_dir, "*.png"))):
126            track_id = _get_track_id(Path(mask_path).stem, name)
127            mask = imageio.imread(mask_path)
128            if mask.ndim == 3:
129                mask = mask[..., 0]
130            labels[mask > MASK_THRESHOLD] = track_id
131
132        imageio.imwrite(output_path, labels, compression="zlib")
133
134    return label_dir
135
136
137def get_svia_data(path: Union[os.PathLike, str], download: bool = False) -> str:
138    """Download the SVIA dataset.
139
140    Args:
141        path: Filepath to a folder where the downloaded data will be saved.
142        download: Whether to download the data if it is not present.
143
144    Returns:
145        The filepath to the extracted data.
146    """
147    data_dir = os.path.join(path, ARCHIVE_ROOT)
148    if os.path.exists(data_dir):
149        return data_dir
150
151    os.makedirs(path, exist_ok=True)
152    archive_path = os.path.join(path, "svia.rar")
153    util.download_source(archive_path, URL, download, CHECKSUM)
154    _extract_archive(archive_path, path)
155
156    return data_dir
157
158
159def get_svia_paths(
160    path: Union[os.PathLike, str],
161    split: Optional[Literal["train", "val", "test"]] = "train",
162    videos: Optional[Sequence[str]] = None,
163    download: bool = False,
164) -> Tuple[List[str], List[str]]:
165    """Get paths to the SVIA data.
166
167    Args:
168        path: Filepath to a folder where the downloaded data will be saved.
169        split: The data split, which groups the videos. Ignored when you pass `videos`.
170        videos: The videos to use, for example ('S_0001',). Overrides `split`.
171        download: Whether to download the data if it is not present.
172
173    Returns:
174        List of filepaths for the image data.
175        List of filepaths for the label data.
176    """
177    if videos is None:
178        if split not in SPLITS:
179            raise ValueError(f"'{split}' is not a valid split. Choose from {list(SPLITS)}, or pass videos.")
180        videos = SPLITS[split]
181    else:
182        for video in videos:
183            if video not in VIDEOS:
184                raise ValueError(f"'{video}' has no masks. Choose from {list(VIDEOS)}.")
185
186    data_dir = get_svia_data(path, download)
187
188    image_paths, label_paths = [], []
189    for video in videos:
190        label_dir = _create_instance_labels(data_dir, video)
191        for label_path in natsorted(glob(os.path.join(label_dir, "*.tif"))):
192            name = Path(label_path).stem
193            image_path = os.path.join(data_dir, "Frames from original videos", video, f"{name}.png")
194            if not os.path.exists(image_path):
195                continue
196            image_paths.append(image_path)
197            label_paths.append(label_path)
198
199    if not image_paths:
200        raise RuntimeError(f"Could not find any SVIA data in {data_dir}.")
201
202    return image_paths, label_paths
203
204
205def get_svia_dataset(
206    path: Union[os.PathLike, str],
207    patch_shape: Tuple[int, int],
208    split: Optional[Literal["train", "val", "test"]] = "train",
209    videos: Optional[Sequence[str]] = None,
210    offsets: Optional[List[List[int]]] = None,
211    boundaries: bool = False,
212    binary: bool = False,
213    download: bool = False,
214    **kwargs,
215) -> Dataset:
216    """Get the SVIA dataset for sperm segmentation.
217
218    Args:
219        path: Filepath to a folder where the downloaded data will be saved.
220        patch_shape: The 2D patch shape to use for training.
221        split: The data split, which groups the videos. Ignored when you pass `videos`.
222        videos: The videos to use, for example ('S_0001',). Overrides `split`.
223        offsets: Offset values for affinity computation used as target.
224        boundaries: Whether to compute boundaries as the target.
225        binary: Whether to use a binary segmentation target.
226        download: Whether to download the data if it is not present.
227        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
228
229    Returns:
230        The segmentation dataset.
231    """
232    if len(patch_shape) != 2:
233        raise ValueError(f"The SVIA patch shape must be two-dimensional, got {patch_shape}.")
234
235    image_paths, label_paths = get_svia_paths(path, split, videos, download)
236
237    kwargs, _ = util.add_instance_label_transform(
238        kwargs, add_binary_target=True, offsets=offsets, boundaries=boundaries, binary=binary,
239    )
240    kwargs = util.ensure_transforms(ndim=2, **kwargs)
241
242    return torch_em.default_segmentation_dataset(
243        raw_paths=image_paths,
244        raw_key=None,
245        label_paths=label_paths,
246        label_key=None,
247        patch_shape=patch_shape,
248        is_seg_dataset=False,
249        ndim=2,
250        **kwargs,
251    )
252
253
254def get_svia_loader(
255    path: Union[os.PathLike, str],
256    batch_size: int,
257    patch_shape: Tuple[int, int],
258    split: Optional[Literal["train", "val", "test"]] = "train",
259    videos: Optional[Sequence[str]] = None,
260    offsets: Optional[List[List[int]]] = None,
261    boundaries: bool = False,
262    binary: bool = False,
263    download: bool = False,
264    **kwargs,
265) -> DataLoader:
266    """Get the SVIA dataloader for sperm segmentation.
267
268    Args:
269        path: Filepath to a folder where the downloaded data will be saved.
270        batch_size: The batch size for training.
271        patch_shape: The 2D patch shape to use for training.
272        split: The data split, which groups the videos. Ignored when you pass `videos`.
273        videos: The videos to use, for example ('S_0001',). Overrides `split`.
274        offsets: Offset values for affinity computation used as target.
275        boundaries: Whether to compute boundaries as the target.
276        binary: Whether to use a binary segmentation target.
277        download: Whether to download the data if it is not present.
278        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or the PyTorch DataLoader.
279
280    Returns:
281        The DataLoader.
282    """
283    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
284    dataset = get_svia_dataset(
285        path=path,
286        patch_shape=patch_shape,
287        split=split,
288        videos=videos,
289        offsets=offsets,
290        boundaries=boundaries,
291        binary=binary,
292        download=download,
293        **ds_kwargs,
294    )
295    return torch_em.get_data_loader(dataset, batch_size=batch_size, **loader_kwargs)
URL = 'https://ndownloader.figshare.com/files/28986378'
CHECKSUM = 'f6956e21eab440806f76a9805655b8a7e56f2555e8f6a03983d2eebab4b2ba71'
ARCHIVE_ROOT = 'Data Set'
VIDEOS = {'S_0001': 45, 'S_0003': 38, 'S_0005': 36, 'S_0006': 26, 'S_0007': 10, 'S_0008': 90, 'S_0009': 48, 'S_0010': 32, 'S_0011': 39, 'S_0012': 87}
SPLITS = {'train': ('S_0001', 'S_0005', 'S_0008', 'S_0010', 'S_0011'), 'val': ('S_0003', 'S_0007'), 'test': ('S_0006', 'S_0009', 'S_0012')}
MASK_THRESHOLD = 127
def get_svia_data(path: Union[os.PathLike, str], download: bool = False) -> str:
138def get_svia_data(path: Union[os.PathLike, str], download: bool = False) -> str:
139    """Download the SVIA dataset.
140
141    Args:
142        path: Filepath to a folder where the downloaded data will be saved.
143        download: Whether to download the data if it is not present.
144
145    Returns:
146        The filepath to the extracted data.
147    """
148    data_dir = os.path.join(path, ARCHIVE_ROOT)
149    if os.path.exists(data_dir):
150        return data_dir
151
152    os.makedirs(path, exist_ok=True)
153    archive_path = os.path.join(path, "svia.rar")
154    util.download_source(archive_path, URL, download, CHECKSUM)
155    _extract_archive(archive_path, path)
156
157    return data_dir

Download the SVIA dataset.

Arguments:
  • path: Filepath to a folder where the downloaded data will be saved.
  • download: Whether to download the data if it is not present.
Returns:

The filepath to the extracted data.

def get_svia_paths( path: Union[os.PathLike, str], split: Optional[Literal['train', 'val', 'test']] = 'train', videos: Optional[Sequence[str]] = None, download: bool = False) -> Tuple[List[str], List[str]]:
160def get_svia_paths(
161    path: Union[os.PathLike, str],
162    split: Optional[Literal["train", "val", "test"]] = "train",
163    videos: Optional[Sequence[str]] = None,
164    download: bool = False,
165) -> Tuple[List[str], List[str]]:
166    """Get paths to the SVIA data.
167
168    Args:
169        path: Filepath to a folder where the downloaded data will be saved.
170        split: The data split, which groups the videos. Ignored when you pass `videos`.
171        videos: The videos to use, for example ('S_0001',). Overrides `split`.
172        download: Whether to download the data if it is not present.
173
174    Returns:
175        List of filepaths for the image data.
176        List of filepaths for the label data.
177    """
178    if videos is None:
179        if split not in SPLITS:
180            raise ValueError(f"'{split}' is not a valid split. Choose from {list(SPLITS)}, or pass videos.")
181        videos = SPLITS[split]
182    else:
183        for video in videos:
184            if video not in VIDEOS:
185                raise ValueError(f"'{video}' has no masks. Choose from {list(VIDEOS)}.")
186
187    data_dir = get_svia_data(path, download)
188
189    image_paths, label_paths = [], []
190    for video in videos:
191        label_dir = _create_instance_labels(data_dir, video)
192        for label_path in natsorted(glob(os.path.join(label_dir, "*.tif"))):
193            name = Path(label_path).stem
194            image_path = os.path.join(data_dir, "Frames from original videos", video, f"{name}.png")
195            if not os.path.exists(image_path):
196                continue
197            image_paths.append(image_path)
198            label_paths.append(label_path)
199
200    if not image_paths:
201        raise RuntimeError(f"Could not find any SVIA data in {data_dir}.")
202
203    return image_paths, label_paths

Get paths to the SVIA data.

Arguments:
  • path: Filepath to a folder where the downloaded data will be saved.
  • split: The data split, which groups the videos. Ignored when you pass videos.
  • videos: The videos to use, for example ('S_0001',). Overrides 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_svia_dataset( path: Union[os.PathLike, str], patch_shape: Tuple[int, int], split: Optional[Literal['train', 'val', 'test']] = 'train', videos: Optional[Sequence[str]] = None, offsets: Optional[List[List[int]]] = None, boundaries: bool = False, binary: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataset.Dataset:
206def get_svia_dataset(
207    path: Union[os.PathLike, str],
208    patch_shape: Tuple[int, int],
209    split: Optional[Literal["train", "val", "test"]] = "train",
210    videos: Optional[Sequence[str]] = None,
211    offsets: Optional[List[List[int]]] = None,
212    boundaries: bool = False,
213    binary: bool = False,
214    download: bool = False,
215    **kwargs,
216) -> Dataset:
217    """Get the SVIA dataset for sperm segmentation.
218
219    Args:
220        path: Filepath to a folder where the downloaded data will be saved.
221        patch_shape: The 2D patch shape to use for training.
222        split: The data split, which groups the videos. Ignored when you pass `videos`.
223        videos: The videos to use, for example ('S_0001',). Overrides `split`.
224        offsets: Offset values for affinity computation used as target.
225        boundaries: Whether to compute boundaries as the target.
226        binary: Whether to use a binary segmentation target.
227        download: Whether to download the data if it is not present.
228        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
229
230    Returns:
231        The segmentation dataset.
232    """
233    if len(patch_shape) != 2:
234        raise ValueError(f"The SVIA patch shape must be two-dimensional, got {patch_shape}.")
235
236    image_paths, label_paths = get_svia_paths(path, split, videos, download)
237
238    kwargs, _ = util.add_instance_label_transform(
239        kwargs, add_binary_target=True, offsets=offsets, boundaries=boundaries, binary=binary,
240    )
241    kwargs = util.ensure_transforms(ndim=2, **kwargs)
242
243    return torch_em.default_segmentation_dataset(
244        raw_paths=image_paths,
245        raw_key=None,
246        label_paths=label_paths,
247        label_key=None,
248        patch_shape=patch_shape,
249        is_seg_dataset=False,
250        ndim=2,
251        **kwargs,
252    )

Get the SVIA dataset for sperm segmentation.

Arguments:
  • path: Filepath to a folder where the downloaded data will be saved.
  • patch_shape: The 2D patch shape to use for training.
  • split: The data split, which groups the videos. Ignored when you pass videos.
  • videos: The videos to use, for example ('S_0001',). Overrides split.
  • offsets: Offset values for affinity computation used as target.
  • boundaries: Whether to compute boundaries as the target.
  • binary: Whether to use a binary segmentation target.
  • 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_svia_loader( path: Union[os.PathLike, str], batch_size: int, patch_shape: Tuple[int, int], split: Optional[Literal['train', 'val', 'test']] = 'train', videos: Optional[Sequence[str]] = None, offsets: Optional[List[List[int]]] = None, boundaries: bool = False, binary: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataloader.DataLoader:
255def get_svia_loader(
256    path: Union[os.PathLike, str],
257    batch_size: int,
258    patch_shape: Tuple[int, int],
259    split: Optional[Literal["train", "val", "test"]] = "train",
260    videos: Optional[Sequence[str]] = None,
261    offsets: Optional[List[List[int]]] = None,
262    boundaries: bool = False,
263    binary: bool = False,
264    download: bool = False,
265    **kwargs,
266) -> DataLoader:
267    """Get the SVIA dataloader for sperm segmentation.
268
269    Args:
270        path: Filepath to a folder where the downloaded data will be saved.
271        batch_size: The batch size for training.
272        patch_shape: The 2D patch shape to use for training.
273        split: The data split, which groups the videos. Ignored when you pass `videos`.
274        videos: The videos to use, for example ('S_0001',). Overrides `split`.
275        offsets: Offset values for affinity computation used as target.
276        boundaries: Whether to compute boundaries as the target.
277        binary: Whether to use a binary segmentation target.
278        download: Whether to download the data if it is not present.
279        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or the PyTorch DataLoader.
280
281    Returns:
282        The DataLoader.
283    """
284    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
285    dataset = get_svia_dataset(
286        path=path,
287        patch_shape=patch_shape,
288        split=split,
289        videos=videos,
290        offsets=offsets,
291        boundaries=boundaries,
292        binary=binary,
293        download=download,
294        **ds_kwargs,
295    )
296    return torch_em.get_data_loader(dataset, batch_size=batch_size, **loader_kwargs)

Get the SVIA dataloader for sperm segmentation.

Arguments:
  • path: Filepath to a folder where the downloaded data will be saved.
  • batch_size: The batch size for training.
  • patch_shape: The 2D patch shape to use for training.
  • split: The data split, which groups the videos. Ignored when you pass videos.
  • videos: The videos to use, for example ('S_0001',). Overrides split.
  • offsets: Offset values for affinity computation used as target.
  • boundaries: Whether to compute boundaries as the target.
  • binary: Whether to use a binary segmentation target.
  • download: Whether to download the data if it is not present.
  • kwargs: Additional keyword arguments for torch_em.default_segmentation_dataset or the PyTorch DataLoader.
Returns:

The DataLoader.