|
| 1 | +"""IXI dataset: ~600 brain MRIs from healthy subjects. |
| 2 | +
|
| 3 | +The `Information eXtraction from Images (IXI) |
| 4 | +<https://brain-development.org/ixi-dataset/>`_ dataset contains |
| 5 | +nearly 600 MR images from normal, healthy subjects. |
| 6 | +
|
| 7 | +This data is made available under the Creative Commons CC BY-SA 3.0 |
| 8 | +license. If you use it, please acknowledge the source. |
| 9 | +""" |
| 10 | + |
| 11 | +from __future__ import annotations |
| 12 | + |
| 13 | +import shutil |
| 14 | +from collections.abc import Sequence |
| 15 | +from pathlib import Path |
| 16 | +from tempfile import NamedTemporaryFile |
| 17 | +from typing import Any |
| 18 | + |
| 19 | +from ..data.image import LabelMap |
| 20 | +from ..data.image import ScalarImage |
| 21 | +from ..data.subject import Subject |
| 22 | +from ..download import download_and_extract_archive |
| 23 | +from ..types import TypePath |
| 24 | + |
| 25 | + |
| 26 | +def ixi( |
| 27 | + root: TypePath, |
| 28 | + *, |
| 29 | + download: bool = False, |
| 30 | + modalities: Sequence[str] = ("T1", "T2"), |
| 31 | +) -> list[Subject]: |
| 32 | + """Download and load the full IXI dataset. |
| 33 | +
|
| 34 | + Args: |
| 35 | + root: Root directory for the dataset. |
| 36 | + download: If ``True``, download the data into ``root``. |
| 37 | + modalities: Modalities to include. Must be a subset of |
| 38 | + ``('T1', 'T2', 'PD', 'MRA', 'DTI')``. |
| 39 | +
|
| 40 | + Returns: |
| 41 | + List of subjects, one per scan with all requested modalities. |
| 42 | +
|
| 43 | + Warning: |
| 44 | + The dataset is several GB. Downloading may take a while. |
| 45 | + """ |
| 46 | + root = Path(root) |
| 47 | + md5s = _IXI_MD5 |
| 48 | + for m in modalities: |
| 49 | + if m not in md5s: |
| 50 | + msg = f'Modality "{m}" must be one of {tuple(md5s.keys())}' |
| 51 | + raise ValueError(msg) |
| 52 | + if download: |
| 53 | + _download_ixi(root, modalities, md5s) |
| 54 | + if not all((root / m).is_dir() for m in modalities): |
| 55 | + msg = "Dataset not found. Use download=True to download it" |
| 56 | + raise RuntimeError(msg) |
| 57 | + return _load_ixi_subjects(root, modalities) |
| 58 | + |
| 59 | + |
| 60 | +def ixi_tiny( |
| 61 | + root: TypePath, |
| 62 | + *, |
| 63 | + download: bool = False, |
| 64 | +) -> list[Subject]: |
| 65 | + r"""Download and load IXITiny (566 $T_1$ images + segmentations). |
| 66 | +
|
| 67 | + All images have shape $83 \times 44 \times 55$. Useful as a |
| 68 | + medical image MNIST for quick experiments. |
| 69 | +
|
| 70 | + Args: |
| 71 | + root: Root directory for the dataset. |
| 72 | + download: If ``True``, download the data into ``root``. |
| 73 | +
|
| 74 | + Returns: |
| 75 | + List of subjects with ``image`` and ``label`` keys. |
| 76 | + """ |
| 77 | + root = Path(root) |
| 78 | + if download: |
| 79 | + _download_ixi_tiny(root) |
| 80 | + if not root.is_dir(): |
| 81 | + msg = "Dataset not found. Use download=True to download it" |
| 82 | + raise RuntimeError(msg) |
| 83 | + return _load_ixi_tiny_subjects(root) |
| 84 | + |
| 85 | + |
| 86 | +# --------------------------------------------------------------------------- |
| 87 | +# Internal helpers |
| 88 | +# --------------------------------------------------------------------------- |
| 89 | + |
| 90 | +_IXI_BASE_URL = ( |
| 91 | + "http://biomedic.doc.ic.ac.uk/brain-development/downloads/IXI/IXI-{modality}.tar" |
| 92 | +) |
| 93 | +_IXI_MD5: dict[str, str] = { |
| 94 | + "T1": "34901a0593b41dd19c1a1f746eac2d58", |
| 95 | + "T2": "e3140d78730ecdd32ba92da48c0a9aaa", |
| 96 | + "PD": "88ecd9d1fa33cb4a2278183b42ffd749", |
| 97 | + "MRA": "29be7d2fee3998f978a55a9bdaf3407e", |
| 98 | + "DTI": "636573825b1c8b9e8c78f1877df3ee66", |
| 99 | +} |
| 100 | + |
| 101 | +_IXI_TINY_URL = "https://www.dropbox.com/s/ogxjwjxdv5mieah/ixi_tiny.zip?dl=1" |
| 102 | +_IXI_TINY_MD5 = "bfb60f4074283d78622760230bfa1f98" |
| 103 | + |
| 104 | + |
| 105 | +def _download_ixi( |
| 106 | + root: Path, |
| 107 | + modalities: Sequence[str], |
| 108 | + md5s: dict[str, str], |
| 109 | +) -> None: |
| 110 | + for modality in modalities: |
| 111 | + modality_dir = root / modality |
| 112 | + if modality_dir.is_dir(): |
| 113 | + continue |
| 114 | + modality_dir.mkdir(exist_ok=True, parents=True) |
| 115 | + url = _IXI_BASE_URL.format(modality=modality) |
| 116 | + with NamedTemporaryFile(suffix=".tar", delete=False) as f: |
| 117 | + download_and_extract_archive( |
| 118 | + url, |
| 119 | + download_root=modality_dir, |
| 120 | + filename=f.name, |
| 121 | + md5=md5s[modality], |
| 122 | + ) |
| 123 | + |
| 124 | + |
| 125 | +def _load_ixi_subjects( |
| 126 | + root: Path, |
| 127 | + modalities: Sequence[str], |
| 128 | +) -> list[Subject]: |
| 129 | + first = modalities[0] |
| 130 | + paths = sorted((root / first).glob("*.nii.gz")) |
| 131 | + subjects: list[Subject] = [] |
| 132 | + for filepath in paths: |
| 133 | + sid = _subject_id(filepath) |
| 134 | + images: dict[str, Any] = {"subject_id": sid} |
| 135 | + images[first] = ScalarImage(filepath) |
| 136 | + skip = False |
| 137 | + for m in modalities[1:]: |
| 138 | + matches = sorted((root / m).glob(f"{sid}-{m}.nii.gz")) |
| 139 | + if matches: |
| 140 | + images[m] = ScalarImage(matches[0]) |
| 141 | + else: |
| 142 | + skip = True |
| 143 | + break |
| 144 | + if not skip: |
| 145 | + subjects.append(Subject(**images)) |
| 146 | + return subjects |
| 147 | + |
| 148 | + |
| 149 | +def _download_ixi_tiny(root: Path) -> None: |
| 150 | + if root.is_dir(): |
| 151 | + return |
| 152 | + with NamedTemporaryFile(suffix=".zip", delete=False) as f: |
| 153 | + download_and_extract_archive( |
| 154 | + _IXI_TINY_URL, |
| 155 | + download_root=root, |
| 156 | + filename=f.name, |
| 157 | + md5=_IXI_TINY_MD5, |
| 158 | + ) |
| 159 | + ixi_tiny_dir = root / "ixi_tiny" |
| 160 | + (ixi_tiny_dir / "image").rename(root / "image") |
| 161 | + (ixi_tiny_dir / "label").rename(root / "label") |
| 162 | + shutil.rmtree(ixi_tiny_dir) |
| 163 | + |
| 164 | + |
| 165 | +def _load_ixi_tiny_subjects(root: Path) -> list[Subject]: |
| 166 | + image_paths = sorted((root / "image").glob("*.nii.gz")) |
| 167 | + label_paths = sorted((root / "label").glob("*.nii.gz")) |
| 168 | + if not (image_paths and label_paths): |
| 169 | + msg = f"Images not found. Remove {root} and try again" |
| 170 | + raise FileNotFoundError(msg) |
| 171 | + subjects: list[Subject] = [] |
| 172 | + for img_path, lbl_path in zip(image_paths, label_paths, strict=True): |
| 173 | + subjects.append( |
| 174 | + Subject( |
| 175 | + image=ScalarImage(img_path), |
| 176 | + label=LabelMap(lbl_path), |
| 177 | + subject_id=_subject_id(img_path), |
| 178 | + ), |
| 179 | + ) |
| 180 | + return subjects |
| 181 | + |
| 182 | + |
| 183 | +def _subject_id(path: Path) -> str: |
| 184 | + return "-".join(path.name.split("-")[:-1]) |
0 commit comments