torch_em.data.datasets.medical.iugc2024
IUGC 2024 is the Intrapartum Ultrasound Grand Challenge dataset for fetal head and pubic symphysis segmentation in transperineal ultrasound videos, recorded to assess the progression of labor. Videos are stored per-frame and only a subset of frames per video is annotated with a pixel-wise, 2-class (fetal head, pubic symphysis) segmentation mask.
The dataset is located at https://www.kaggle.com/datasets/aspirexxx/iugc-ultrasound-video-dataset-miccai-2024. This dataset is from the publication https://doi.org/10.1007/978-3-031-96318-6_1. Please cite it if you use this dataset for your research.
NOTE: The dataset stores raw frames as videos (.avi) instead of individual images, so
this module extracts the specific annotated frame(s) out of each video and caches it to
disk as an image next to the corresponding mask. The "train" split stores masks nested
per-video (seg/<video_stem>/mask/<video_stem>_<frame_index>_6.png) and its video files
are named <recording_id>__<video_stem>.avi, unlike the flat layout of "val" and "test".
1"""IUGC 2024 is the Intrapartum Ultrasound Grand Challenge dataset for fetal head and 2pubic symphysis segmentation in transperineal ultrasound videos, recorded to assess the 3progression of labor. Videos are stored per-frame and only a subset of frames per video 4is annotated with a pixel-wise, 2-class (fetal head, pubic symphysis) segmentation mask. 5 6The dataset is located at 7https://www.kaggle.com/datasets/aspirexxx/iugc-ultrasound-video-dataset-miccai-2024. 8This dataset is from the publication https://doi.org/10.1007/978-3-031-96318-6_1. 9Please cite it if you use this dataset for your research. 10 11NOTE: The dataset stores raw frames as videos (`.avi`) instead of individual images, so 12this module extracts the specific annotated frame(s) out of each video and caches it to 13disk as an image next to the corresponding mask. The "train" split stores masks nested 14per-video (`seg/<video_stem>/mask/<video_stem>_<frame_index>_6.png`) and its video files 15are named `<recording_id>__<video_stem>.avi`, unlike the flat layout of "val" and "test". 16""" 17 18import os 19import csv 20from glob import glob 21from typing import Union, Tuple, List, Literal 22 23import imageio.v3 as imageio 24 25from torch.utils.data import Dataset, DataLoader 26 27import torch_em 28 29from .. import util 30 31 32KAGGLE_DATASET_NAME = "aspirexxx/iugc-ultrasound-video-dataset-miccai-2024" 33 34# The dataset does not expose a stable top-level folder layout, so the per-split prefixes 35# below were resolved once via the Kaggle Files API (`KaggleApi.dataset_list_files`). 36SPLIT_PREFIXES = { 37 "train": "DatasetV3/train-20251119T060603Z-1-001/train/", 38 "val": "DatasetV3/val-20251119T054616Z-1-001/val/", 39 "test": "DatasetV3/test-20251119T054614Z-1-001/test/", 40} 41 42 43def _get_kaggle_api(): 44 try: 45 from kaggle.api.kaggle_api_extended import KaggleApi 46 except ModuleNotFoundError: 47 msg = "Please install the Kaggle API. You can do this using 'pip install kaggle'. " 48 msg += "After you have installed kaggle, you would need an API token. " 49 msg += "Follow the instructions at https://www.kaggle.com/docs/api." 50 raise ModuleNotFoundError(msg) 51 52 api = KaggleApi() 53 api.authenticate() 54 return api 55 56 57def _list_seg_filenames(api, prefix): 58 seg_prefix = f"{prefix}seg/" 59 filenames = [] 60 token = None 61 while True: 62 response = api.dataset_list_files(KAGGLE_DATASET_NAME, page_token=token, page_size=500) 63 names = [f.name for f in response.files] 64 for name in names: 65 if name.startswith(seg_prefix) and name.endswith(".png"): 66 filenames.append(os.path.basename(name)) 67 68 # The listing is alphabetically ordered, so once we have moved past the "seg/" folder 69 # (and already collected some file names) we can stop early. 70 if filenames and not any(name.startswith(seg_prefix) for name in names): 71 break 72 73 token = response.next_page_token 74 if not token: 75 break 76 77 return filenames 78 79 80def _list_all_filenames(api): 81 filenames = [] 82 token = None 83 while True: 84 response = api.dataset_list_files(KAGGLE_DATASET_NAME, page_token=token, page_size=500) 85 filenames.extend(f.name for f in response.files) 86 token = response.next_page_token 87 if not token: 88 break 89 90 return filenames 91 92 93def _match_train_video_name(video_names, video_stem): 94 # Train videos are named "<recording_id>__<video_stem>.avi", unlike the flat 95 # "<video_stem>.avi" naming used for the "val" and "test" splits. 96 matches = [name for name in video_names if os.path.splitext(name)[0].rsplit("__", 1)[-1] == video_stem] 97 if len(matches) != 1: 98 raise RuntimeError( 99 f"Found {len(matches)} candidate video files for '{video_stem}' in the train split, expected exactly 1." 100 ) 101 102 return matches[0] 103 104 105def _download_train_data(api, prefix, rows, videos_dir, seg_dir): 106 all_filenames = _list_all_filenames(api) 107 108 videos_prefix = f"{prefix}videos/" 109 video_names = [name[len(videos_prefix):] for name in all_filenames if name.startswith(videos_prefix)] 110 111 seg_prefix = f"{prefix}seg/" 112 mask_names = [ 113 name[len(seg_prefix):] for name in all_filenames 114 if name.startswith(seg_prefix) and name.endswith(".png") and "/mask/" in name 115 ] 116 117 for row in rows: 118 video_stem = os.path.splitext(row["filename"])[0] 119 120 video_name = _match_train_video_name(video_names, video_stem) 121 video_path = os.path.join(videos_dir, video_name) 122 if not os.path.exists(video_path): 123 api.dataset_download_file(KAGGLE_DATASET_NAME, f"{videos_prefix}{video_name}", path=videos_dir, quiet=False) 124 125 video_mask_names = [name for name in mask_names if name.startswith(f"{video_stem}/mask/")] 126 for mask_name in video_mask_names: 127 mask_path = os.path.join(seg_dir, os.path.basename(mask_name)) 128 if os.path.exists(mask_path): 129 continue 130 api.dataset_download_file(KAGGLE_DATASET_NAME, f"{seg_prefix}{mask_name}", path=seg_dir, quiet=False) 131 132 133def get_iugc2024_data( 134 path: Union[os.PathLike, str], split: Literal["train", "val", "test"], download: bool = False 135) -> str: 136 """Download the IUGC 2024 dataset. 137 138 Args: 139 path: Filepath to a folder where the data is downloaded for further processing. 140 split: The choice of data split. 141 download: Whether to download the data if it is not present. 142 143 Returns: 144 Filepath where the data is downloaded. 145 """ 146 if split not in SPLIT_PREFIXES: 147 raise ValueError(f"'{split}' is not a supported split. Choose one of {list(SPLIT_PREFIXES.keys())}.") 148 149 data_dir = os.path.join(path, split) 150 videos_dir = os.path.join(data_dir, "videos") 151 seg_dir = os.path.join(data_dir, "seg") 152 os.makedirs(videos_dir, exist_ok=True) 153 os.makedirs(seg_dir, exist_ok=True) 154 155 info_path = os.path.join(data_dir, "seg_info.csv") 156 if os.path.exists(info_path): 157 return data_dir 158 159 if not download: 160 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.") 161 162 prefix = SPLIT_PREFIXES[split] 163 api = _get_kaggle_api() 164 165 api.dataset_download_file(KAGGLE_DATASET_NAME, f"{prefix}seg/seg_info.csv", path=data_dir, quiet=False) 166 167 with open(info_path) as f: 168 rows = list(csv.DictReader(f)) 169 170 if split == "train": 171 _download_train_data(api, prefix, rows, videos_dir, seg_dir) 172 else: 173 seg_filenames = _list_seg_filenames(api, prefix) 174 for row in rows: 175 video_name = row["filename"] 176 video_path = os.path.join(videos_dir, video_name) 177 if not os.path.exists(video_path): 178 api.dataset_download_file( 179 KAGGLE_DATASET_NAME, f"{prefix}videos/{video_name}", path=videos_dir, quiet=False 180 ) 181 182 video_stem = os.path.splitext(video_name)[0] 183 matches = [name for name in seg_filenames if name.startswith(video_stem)] 184 for mask_name in matches: 185 mask_path = os.path.join(seg_dir, mask_name) 186 if os.path.exists(mask_path): 187 continue 188 api.dataset_download_file(KAGGLE_DATASET_NAME, f"{prefix}seg/{mask_name}", path=seg_dir, quiet=False) 189 190 return data_dir 191 192 193def get_iugc2024_paths( 194 path: Union[os.PathLike, str], split: Literal["train", "val", "test"], download: bool = False 195) -> Tuple[List[str], List[str]]: 196 """Get paths to the IUGC 2024 data. 197 198 Args: 199 path: Filepath to a folder where the data is downloaded for further processing. 200 split: The choice of data split. 201 download: Whether to download the data if it is not present. 202 203 Returns: 204 List of filepaths for the image data. 205 List of filepaths for the label data. 206 """ 207 import cv2 208 209 data_dir = get_iugc2024_data(path=path, split=split, download=download) 210 videos_dir = os.path.join(data_dir, "videos") 211 seg_dir = os.path.join(data_dir, "seg") 212 213 frames_dir = os.path.join(data_dir, "frames") 214 os.makedirs(frames_dir, exist_ok=True) 215 216 mask_paths = sorted(glob(os.path.join(seg_dir, "*.png"))) 217 218 image_paths, gt_paths = [], [] 219 for mask_path in mask_paths: 220 mask_stem = os.path.splitext(os.path.basename(mask_path))[0] 221 frame_path = os.path.join(frames_dir, f"{mask_stem}.tif") 222 223 image_paths.append(frame_path) 224 gt_paths.append(mask_path) 225 if os.path.exists(frame_path): 226 continue 227 228 if split == "train": 229 # The mask stem is "<video_stem>_<frame_index>_6" and the corresponding video is 230 # named "<recording_id>__<video_stem>.avi". 231 video_stem, frame_index, _ = mask_stem.rsplit("_", 2) 232 video_candidates = glob(os.path.join(videos_dir, f"*__{video_stem}.avi")) 233 if not video_candidates: 234 raise RuntimeError(f"Could not find a video file for the mask stem '{mask_stem}'.") 235 video_path = video_candidates[0] 236 frame_index = int(frame_index) 237 else: 238 # The mask stem is either "<video_stem>" (val split) or "<video_stem>_<frame_index>" (test split). 239 video_candidates = glob(os.path.join(videos_dir, f"{mask_stem}.avi")) 240 if video_candidates: 241 video_path = video_candidates[0] 242 frame_index = 0 243 else: 244 video_stem, frame_index = mask_stem.rsplit("_", 1) 245 video_path = os.path.join(videos_dir, f"{video_stem}.avi") 246 frame_index = int(frame_index) 247 248 capture = cv2.VideoCapture(video_path) 249 capture.set(cv2.CAP_PROP_POS_FRAMES, frame_index) 250 success, frame = capture.read() 251 capture.release() 252 if not success: 253 raise RuntimeError(f"Could not read frame {frame_index} from '{video_path}'.") 254 255 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 256 imageio.imwrite(frame_path, frame, compression="zlib") 257 258 return image_paths, gt_paths 259 260 261def get_iugc2024_dataset( 262 path: Union[os.PathLike, str], 263 patch_shape: Tuple[int, int], 264 split: Literal["train", "val", "test"], 265 resize_inputs: bool = False, 266 download: bool = False, 267 **kwargs 268) -> Dataset: 269 """Get the IUGC 2024 dataset for fetal head and pubic symphysis segmentation. 270 271 Args: 272 path: Filepath to a folder where the data is downloaded for further processing. 273 patch_shape: The patch shape to use for training. 274 split: The choice of data split. 275 resize_inputs: Whether to resize the inputs to the patch shape. 276 download: Whether to download the data if it is not present. 277 kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`. 278 279 Returns: 280 The segmentation dataset. 281 """ 282 image_paths, gt_paths = get_iugc2024_paths(path, split, download) 283 284 if resize_inputs: 285 resize_kwargs = {"patch_shape": patch_shape, "is_rgb": True} 286 kwargs, patch_shape = util.update_kwargs_for_resize_trafo( 287 kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs 288 ) 289 290 return torch_em.default_segmentation_dataset( 291 raw_paths=image_paths, 292 raw_key=None, 293 label_paths=gt_paths, 294 label_key=None, 295 patch_shape=patch_shape, 296 is_seg_dataset=False, 297 **kwargs 298 ) 299 300 301def get_iugc2024_loader( 302 path: Union[os.PathLike, str], 303 patch_shape: Tuple[int, int], 304 batch_size: int, 305 split: Literal["train", "val", "test"], 306 resize_inputs: bool = False, 307 download: bool = False, 308 **kwargs 309) -> DataLoader: 310 """Get the IUGC 2024 dataloader for fetal head and pubic symphysis segmentation. 311 312 Args: 313 path: Filepath to a folder where the data is downloaded for further processing. 314 patch_shape: The patch shape to use for training. 315 batch_size: The batch size for training. 316 split: The choice of data split. 317 resize_inputs: Whether to resize the inputs to the patch shape. 318 download: Whether to download the data if it is not present. 319 kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader. 320 321 Returns: 322 The DataLoader. 323 """ 324 ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs) 325 dataset = get_iugc2024_dataset(path, patch_shape, split, resize_inputs, download, **ds_kwargs) 326 return torch_em.get_data_loader(dataset=dataset, batch_size=batch_size, **loader_kwargs)
134def get_iugc2024_data( 135 path: Union[os.PathLike, str], split: Literal["train", "val", "test"], download: bool = False 136) -> str: 137 """Download the IUGC 2024 dataset. 138 139 Args: 140 path: Filepath to a folder where the data is downloaded for further processing. 141 split: The choice of data split. 142 download: Whether to download the data if it is not present. 143 144 Returns: 145 Filepath where the data is downloaded. 146 """ 147 if split not in SPLIT_PREFIXES: 148 raise ValueError(f"'{split}' is not a supported split. Choose one of {list(SPLIT_PREFIXES.keys())}.") 149 150 data_dir = os.path.join(path, split) 151 videos_dir = os.path.join(data_dir, "videos") 152 seg_dir = os.path.join(data_dir, "seg") 153 os.makedirs(videos_dir, exist_ok=True) 154 os.makedirs(seg_dir, exist_ok=True) 155 156 info_path = os.path.join(data_dir, "seg_info.csv") 157 if os.path.exists(info_path): 158 return data_dir 159 160 if not download: 161 raise RuntimeError(f"Cannot find the data at {path}, but download was set to False.") 162 163 prefix = SPLIT_PREFIXES[split] 164 api = _get_kaggle_api() 165 166 api.dataset_download_file(KAGGLE_DATASET_NAME, f"{prefix}seg/seg_info.csv", path=data_dir, quiet=False) 167 168 with open(info_path) as f: 169 rows = list(csv.DictReader(f)) 170 171 if split == "train": 172 _download_train_data(api, prefix, rows, videos_dir, seg_dir) 173 else: 174 seg_filenames = _list_seg_filenames(api, prefix) 175 for row in rows: 176 video_name = row["filename"] 177 video_path = os.path.join(videos_dir, video_name) 178 if not os.path.exists(video_path): 179 api.dataset_download_file( 180 KAGGLE_DATASET_NAME, f"{prefix}videos/{video_name}", path=videos_dir, quiet=False 181 ) 182 183 video_stem = os.path.splitext(video_name)[0] 184 matches = [name for name in seg_filenames if name.startswith(video_stem)] 185 for mask_name in matches: 186 mask_path = os.path.join(seg_dir, mask_name) 187 if os.path.exists(mask_path): 188 continue 189 api.dataset_download_file(KAGGLE_DATASET_NAME, f"{prefix}seg/{mask_name}", path=seg_dir, quiet=False) 190 191 return data_dir
Download the IUGC 2024 dataset.
Arguments:
- path: Filepath to a folder where the data is downloaded for further processing.
- split: The choice of data split.
- download: Whether to download the data if it is not present.
Returns:
Filepath where the data is downloaded.
194def get_iugc2024_paths( 195 path: Union[os.PathLike, str], split: Literal["train", "val", "test"], download: bool = False 196) -> Tuple[List[str], List[str]]: 197 """Get paths to the IUGC 2024 data. 198 199 Args: 200 path: Filepath to a folder where the data is downloaded for further processing. 201 split: The choice of data split. 202 download: Whether to download the data if it is not present. 203 204 Returns: 205 List of filepaths for the image data. 206 List of filepaths for the label data. 207 """ 208 import cv2 209 210 data_dir = get_iugc2024_data(path=path, split=split, download=download) 211 videos_dir = os.path.join(data_dir, "videos") 212 seg_dir = os.path.join(data_dir, "seg") 213 214 frames_dir = os.path.join(data_dir, "frames") 215 os.makedirs(frames_dir, exist_ok=True) 216 217 mask_paths = sorted(glob(os.path.join(seg_dir, "*.png"))) 218 219 image_paths, gt_paths = [], [] 220 for mask_path in mask_paths: 221 mask_stem = os.path.splitext(os.path.basename(mask_path))[0] 222 frame_path = os.path.join(frames_dir, f"{mask_stem}.tif") 223 224 image_paths.append(frame_path) 225 gt_paths.append(mask_path) 226 if os.path.exists(frame_path): 227 continue 228 229 if split == "train": 230 # The mask stem is "<video_stem>_<frame_index>_6" and the corresponding video is 231 # named "<recording_id>__<video_stem>.avi". 232 video_stem, frame_index, _ = mask_stem.rsplit("_", 2) 233 video_candidates = glob(os.path.join(videos_dir, f"*__{video_stem}.avi")) 234 if not video_candidates: 235 raise RuntimeError(f"Could not find a video file for the mask stem '{mask_stem}'.") 236 video_path = video_candidates[0] 237 frame_index = int(frame_index) 238 else: 239 # The mask stem is either "<video_stem>" (val split) or "<video_stem>_<frame_index>" (test split). 240 video_candidates = glob(os.path.join(videos_dir, f"{mask_stem}.avi")) 241 if video_candidates: 242 video_path = video_candidates[0] 243 frame_index = 0 244 else: 245 video_stem, frame_index = mask_stem.rsplit("_", 1) 246 video_path = os.path.join(videos_dir, f"{video_stem}.avi") 247 frame_index = int(frame_index) 248 249 capture = cv2.VideoCapture(video_path) 250 capture.set(cv2.CAP_PROP_POS_FRAMES, frame_index) 251 success, frame = capture.read() 252 capture.release() 253 if not success: 254 raise RuntimeError(f"Could not read frame {frame_index} from '{video_path}'.") 255 256 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 257 imageio.imwrite(frame_path, frame, compression="zlib") 258 259 return image_paths, gt_paths
Get paths to the IUGC 2024 data.
Arguments:
- path: Filepath to a folder where the data is downloaded for further processing.
- split: The choice of data 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.
262def get_iugc2024_dataset( 263 path: Union[os.PathLike, str], 264 patch_shape: Tuple[int, int], 265 split: Literal["train", "val", "test"], 266 resize_inputs: bool = False, 267 download: bool = False, 268 **kwargs 269) -> Dataset: 270 """Get the IUGC 2024 dataset for fetal head and pubic symphysis segmentation. 271 272 Args: 273 path: Filepath to a folder where the data is downloaded for further processing. 274 patch_shape: The patch shape to use for training. 275 split: The choice of data split. 276 resize_inputs: Whether to resize the inputs to the patch shape. 277 download: Whether to download the data if it is not present. 278 kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`. 279 280 Returns: 281 The segmentation dataset. 282 """ 283 image_paths, gt_paths = get_iugc2024_paths(path, split, download) 284 285 if resize_inputs: 286 resize_kwargs = {"patch_shape": patch_shape, "is_rgb": True} 287 kwargs, patch_shape = util.update_kwargs_for_resize_trafo( 288 kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs 289 ) 290 291 return torch_em.default_segmentation_dataset( 292 raw_paths=image_paths, 293 raw_key=None, 294 label_paths=gt_paths, 295 label_key=None, 296 patch_shape=patch_shape, 297 is_seg_dataset=False, 298 **kwargs 299 )
Get the IUGC 2024 dataset for fetal head and pubic symphysis 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.
- resize_inputs: Whether to resize the inputs to the 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.
302def get_iugc2024_loader( 303 path: Union[os.PathLike, str], 304 patch_shape: Tuple[int, int], 305 batch_size: int, 306 split: Literal["train", "val", "test"], 307 resize_inputs: bool = False, 308 download: bool = False, 309 **kwargs 310) -> DataLoader: 311 """Get the IUGC 2024 dataloader for fetal head and pubic symphysis segmentation. 312 313 Args: 314 path: Filepath to a folder where the data is downloaded for further processing. 315 patch_shape: The patch shape to use for training. 316 batch_size: The batch size for training. 317 split: The choice of data split. 318 resize_inputs: Whether to resize the inputs to the patch shape. 319 download: Whether to download the data if it is not present. 320 kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader. 321 322 Returns: 323 The DataLoader. 324 """ 325 ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs) 326 dataset = get_iugc2024_dataset(path, patch_shape, split, resize_inputs, download, **ds_kwargs) 327 return torch_em.get_data_loader(dataset=dataset, batch_size=batch_size, **loader_kwargs)
Get the IUGC 2024 dataloader for fetal head and pubic symphysis segmentation.
Arguments:
- path: Filepath to a folder where the data is downloaded for further processing.
- patch_shape: The patch shape to use for training.
- batch_size: The batch size for training.
- split: The choice of data split.
- resize_inputs: Whether to resize the inputs to the patch shape.
- download: Whether to download the data if it is not present.
- kwargs: Additional keyword arguments for
torch_em.default_segmentation_datasetor for the PyTorch DataLoader.
Returns:
The DataLoader.