File size: 15,516 Bytes
1cf3825
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
"""Fetch out-of-distribution brain MRI samples for testing the deployed system.



What "OOD" means here: NOT from BraTS 2020, kaggle_3m LGG, Figshare-Cheng-2017,

or Kaggle 4-class — i.e. nothing the v8 / v5 / v3 cascade has ever seen.



Sources chosen:

  1. g4m3r/T1w_MRI_Brain_Slices (HF)  - OpenNeuro ds003592 / Spreng et al.

     neurocognitive aging study. 301 healthy adults, 30 coronal slices each.

     MIT license. Public, ungated. PNG. -> samples/ood/healthy_coronal_T1

  2. FOMO25/FOMO-MRI (HF)             - OASIS-1/OASIS-2 + others. T1, T2,

     FLAIR, T1c, PD, etc. CC BY-NC-SA 4.0. Gated (auto-approve). NIfTI.

     We extract middle slices per modality -> samples/ood/multimodal_oasis

  3. UniDataPro/brain-cancer-dataset (HF) - Proprietary DICOM tumor study.

     CC BY-NC-ND-4.0. Public. -> samples/ood/tumor_proprietary_dicom



Why coronal slices from #1: most of our training data is axial. Coronal is

a real distribution shift on top of the source-OOD shift, which is exactly

the kind of stress test the user asked for.



If a download fails (e.g. FOMO is still gated or the network is slow), the

script keeps going and reports what it managed to grab.

"""
from __future__ import annotations

import io
import os
import sys
import time
from pathlib import Path
from typing import List, Optional

ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / 'samples' / 'ood'
OUT.mkdir(parents=True, exist_ok=True)


def _hf_api(token: Optional[str] = None):
    from huggingface_hub import HfApi
    tok = token or os.environ.get('HF_TOKEN')
    return HfApi(token=tok)


def fetch_openneuro_t1(n: int = 15) -> List[Path]:
    """Grab N healthy T1w coronal PNGs from g4m3r/T1w_MRI_Brain_Slices.



    The dataset ships as a single images.zip (~352 MB). Download once,

    extract only the slices we want, then delete the zip to keep the

    samples folder small. 30 slices per subject -> stride 30 = one per

    subject so we get anatomical diversity, not 12 slices of one person.

    """
    from huggingface_hub import hf_hub_download
    import zipfile
    target = OUT / 'healthy_coronal_T1_openneuro'
    target.mkdir(parents=True, exist_ok=True)
    out: List[Path] = []
    # Reuse the zip if we already downloaded it in a previous (failed) run.
    cached_zip = ROOT / 'samples' / 'ood' / '_zip_tmp' / 'images.zip'
    if cached_zip.exists() and cached_zip.stat().st_size > 100_000_000:
        zpath = str(cached_zip)
        print(f'  reusing cached zip: {cached_zip}')
    else:
        print('  downloading images.zip (one-time, ~352 MB)...')
        try:
            zpath = hf_hub_download(
                repo_id='g4m3r/T1w_MRI_Brain_Slices',
                filename='images.zip',
                repo_type='dataset',
                local_dir=str(cached_zip.parent),
            )
        except Exception as exc:
            print(f'  [fail-zip] {type(exc).__name__}: {exc}')
            return []
    # Actual zip layout: images/sub-XX_slice_YYY.png  (X 01..99, Y mid ~170)
    import re
    pat = re.compile(r'sub-(\d+)_slice_(\d+)\.png$')
    target_slice = 169  # middle-ish of the [141..199] range present
    chosen_subjects: set[str] = set()
    with zipfile.ZipFile(zpath) as zf:
        # Sort so we pick subjects in a deterministic spread (every 7th sub).
        candidates = sorted(zf.namelist())
        stride = max(1, 99 // n)
        wanted_subs = {f'{(1 + stride*k):02d}' for k in range(n)}
        for nm in candidates:
            m = pat.search(nm.rsplit('/', 1)[-1])
            if not m:
                continue
            sub, sl = m.group(1), int(m.group(2))
            if sub not in wanted_subs:
                continue
            if sub in chosen_subjects:
                continue
            if sl != target_slice:
                continue
            base = nm.rsplit('/', 1)[-1]
            with zf.open(nm) as src:
                (target / base).write_bytes(src.read())
            out.append(target / base)
            chosen_subjects.add(sub)
            print(f'  [extract] {base}')
            if len(out) >= n:
                break
    # Clean up the big zip so the samples folder stays slim.
    try:
        Path(zpath).unlink()
        # Remove the empty .cache subfolder hf_hub created if present.
        for c in target.glob('.cache'):
            import shutil; shutil.rmtree(c, ignore_errors=True)
    except Exception:
        pass
    return out


def fetch_fomo_multimodal(n_subjects: int = 2,

                           modalities: List[str] = ('t1', 't2', 'flair')) -> List[Path]:
    """Try to grab 1-2 subjects' multi-modal NIfTIs from FOMO50K and

    extract middle slices per modality.



    Gated dataset; if denied, returns []."""
    try:
        from huggingface_hub import hf_hub_download, HfApi
        import nibabel as nib
        import numpy as np
        from PIL import Image
    except Exception as exc:
        print(f'  [skip-fomo] dep missing: {exc}')
        return []

    target = OUT / 'multimodal_oasis_fomo'
    target.mkdir(parents=True, exist_ok=True)
    out: List[Path] = []
    api = _hf_api()

    # Probe a couple of plausible paths -- the README sample shows
    # PT001_OASIS1/sub_52/ses_1/t1.nii.gz layout.
    probes = []
    for ptn in ('PT001_OASIS1', 'PT002_OASIS2'):
        for sub_n in (1, 2, 5, 10, 27, 52):
            for mod in modalities:
                probes.append(f'{ptn}/sub_{sub_n}/ses_1/{mod}.nii.gz')
    # Build sets per subject so we only keep ones where ALL modalities resolved.
    by_subject: dict[str, dict[str, Path]] = {}
    for path in probes:
        subj = '/'.join(path.split('/')[:2])
        mod = path.split('/')[-1].replace('.nii.gz', '')
        try:
            p = hf_hub_download(
                repo_id='FOMO25/FOMO-MRI',
                filename=path,
                repo_type='dataset',
                local_dir=str(target),
                local_dir_use_symlinks=False,
            )
            by_subject.setdefault(subj, {})[mod] = Path(p)
            print(f'  [ok-fomo] {path}')
            if len([k for k, v in by_subject.items() if len(v) >= len(modalities)]) >= n_subjects:
                break
        except Exception as exc:
            print(f'  [miss-fomo] {path}: {type(exc).__name__}')

    # For subjects where we got every modality, slice mid-axial + save PNG
    for subj, mods in by_subject.items():
        if len(mods) < len(modalities):
            continue
        subj_tag = subj.replace('/', '_')
        for mod_name, nii_path in mods.items():
            try:
                arr = nib.load(str(nii_path)).get_fdata()
                # take mid-axial slice (assume axes: x, y, z)
                z = arr.shape[2] // 2
                sl = arr[:, :, z]
                # 99th-pct normalize -> uint8
                p99 = np.percentile(sl, 99) or 1.0
                sl = np.clip(sl / p99, 0, 1) * 255
                img = Image.fromarray(sl.astype('uint8'))
                out_png = target / f'{subj_tag}__{mod_name}.png'
                img.save(out_png)
                out.append(out_png)
                print(f'  [save] {out_png.name}')
            except Exception as exc:
                print(f'  [slice-fail] {nii_path.name}: {exc}')
    return out


def fetch_unidata_dicom(n_per_series: int = 2) -> List[Path]:
    """Pull DICOMs from EACH of the UniDataPro Series folders (SE000001 ..

    SE000009+). Each Series is typically a different MRI sequence (T1, T2,

    FLAIR, DWI etc.) — which gives us the multi-CHANNEL/multi-MODALITY

    diversity the user asked for, on a proprietary OOD source.



    Reads SeriesDescription / Modality from the DICOM header and prefixes

    the saved PNG so the eval can see which sequence it was.

    """
    try:
        from huggingface_hub import hf_hub_download, list_repo_files
        import pydicom
        import numpy as np
        from PIL import Image
    except ImportError as exc:
        if 'pydicom' in str(exc):
            print('  [skip-unidata] pydicom not installed; pip install pydicom')
        else:
            print(f'  [skip-unidata] dep missing: {exc}')
        return []

    target = OUT / 'tumor_proprietary_multimodal_unidata'
    target.mkdir(parents=True, exist_ok=True)
    out: List[Path] = []
    try:
        files = list_repo_files('UniDataPro/brain-cancer-dataset', repo_type='dataset')
        # .dcm only; group by series (SE000001 .. SE000009)
        dcm = sorted(f for f in files if f.lower().endswith('.dcm'))
        by_series: dict[str, List[str]] = {}
        for f in dcm:
            parts = f.split('/')
            if len(parts) >= 3 and parts[1].startswith('SE'):
                by_series.setdefault(parts[1], []).append(f)
        print(f'  found {len(by_series)} series in repo: {sorted(by_series.keys())}')
        for series, members in sorted(by_series.items()):
            # Pick mid-series + a second offset for slice variety
            mid = len(members) // 2
            picks = [members[mid]]
            if len(members) > 4 and n_per_series > 1:
                picks.append(members[mid // 2])
            for f in picks[:n_per_series]:
                try:
                    p = hf_hub_download(
                        repo_id='UniDataPro/brain-cancer-dataset',
                        filename=f,
                        repo_type='dataset',
                        local_dir=str(target),
                    )
                except Exception as exc:
                    print(f'  [dl-fail] {f}: {type(exc).__name__}')
                    continue
                try:
                    d = pydicom.dcmread(p, force=True)
                    # Pull the actual sequence tag for downstream labelling.
                    sd = str(getattr(d, 'SeriesDescription', '') or
                              getattr(d, 'ProtocolName', '') or
                              getattr(d, 'Modality', '') or 'unk').strip()
                    sd_clean = ''.join(c for c in sd if c.isalnum() or c in '._-')[:40] or 'unk'
                    arr = d.pixel_array.astype('float32')
                    p99 = float(np.percentile(arr, 99) or 1.0)
                    arr = np.clip(arr / p99, 0, 1) * 255
                    out_name = f'{series}__{sd_clean}__{Path(p).stem}.png'
                    out_png = target / out_name
                    Image.fromarray(arr.astype('uint8')).save(out_png)
                    out.append(out_png)
                    print(f'  [ok-unidata] {series}/{Path(p).name} '
                          f'(SeriesDescription={sd!r}) -> {out_name}')
                except Exception as exc:
                    print(f'  [dicom-fail] {f}: {type(exc).__name__}: {exc}')
    except Exception as exc:
        print(f'  [skip-unidata] list_repo_files: {exc}')
    return out


def fetch_ultralytics_tumor_patients(n_patients: int = 10) -> List[Path]:
    """Pull one image per distinct patient prefix from Ultralytics/Brain-tumor.



    Filenames are <patient_id>_<frame>.jpg (e.g. 00054_145.jpg). One image

    per patient -> n_patients distinct OOD tumor patients. AGPL-3.0.

    """
    from huggingface_hub import hf_hub_download, list_repo_files
    target = OUT / 'tumor_multi_patient_ultralytics'
    target.mkdir(parents=True, exist_ok=True)
    out: List[Path] = []
    try:
        files = list_repo_files('Ultralytics/Brain-tumor', repo_type='dataset')
    except Exception as exc:
        print(f'  [skip-ult] {exc}')
        return []
    imgs = sorted(f for f in files if f.startswith('train/images/') and f.endswith('.jpg'))
    # Group by patient prefix
    seen: dict[str, str] = {}
    for f in imgs:
        base = f.rsplit('/', 1)[-1]
        pid = base.split('_', 1)[0]
        if pid not in seen:
            seen[pid] = f
        if len(seen) >= n_patients:
            break
    for pid, f in seen.items():
        try:
            p = hf_hub_download(
                repo_id='Ultralytics/Brain-tumor',
                filename=f,
                repo_type='dataset',
                local_dir=str(target),
            )
            # Flatten the train/images/ prefix so the eval picks them up.
            flat = target / f'pt{pid}__{Path(p).name}'
            Path(p).rename(flat)
            out.append(flat)
            print(f'  [ok-ult] patient={pid} -> {flat.name}')
        except Exception as exc:
            print(f'  [fail-ult] {f}: {type(exc).__name__}: {exc}')
    return out


def fetch_navoneel_binary(n: int = 6) -> List[Path]:
    """Pull N Y*.jpg (tumor-positive) images from miladfa7's mirror of

    Navoneel Chakrabarty's binary brain-tumor-detection set."""
    from huggingface_hub import hf_hub_download
    import zipfile
    target = OUT / 'tumor_binary_navoneel_via_miladfa7'
    target.mkdir(parents=True, exist_ok=True)
    out: List[Path] = []
    try:
        zpath = hf_hub_download(
            repo_id='miladfa7/Brain-MRI-Images-for-Brain-Tumor-Detection',
            filename='Brain MRI Images for Brain Tumor Detection.zip',
            repo_type='dataset',
            local_dir=str(target),
        )
    except Exception as exc:
        print(f'  [skip-nav] {exc}')
        return []
    with zipfile.ZipFile(zpath) as zf:
        # Navoneel layout: "yes/Y1.jpg .. Y155.jpg" + "no/N1.jpg .."
        yes = sorted(n for n in zf.namelist()
                      if '/yes/' in n.lower() and n.lower().endswith(('.jpg', '.png')))
        if not yes:
            yes = sorted(n for n in zf.namelist()
                          if 'y' in n.rsplit('/', 1)[-1].lower()[:2]
                          and n.lower().endswith(('.jpg', '.png')))
        step = max(1, len(yes) // n)
        picks = yes[::step][:n]
        for nm in picks:
            base = nm.rsplit('/', 1)[-1]
            with zf.open(nm) as src:
                (target / base).write_bytes(src.read())
            out.append(target / base)
            print(f'  [ok-nav] {base}')
    try:
        Path(zpath).unlink()
    except Exception:
        pass
    return out


def main():
    t0 = time.perf_counter()
    print('=== Fetching OOD brain MRI samples ===')
    print()
    print('-> 1/5 OpenNeuro healthy T1 coronal (g4m3r/T1w_MRI_Brain_Slices)')
    a = fetch_openneuro_t1(n=12)
    print()
    print('-> 2/5 OASIS multi-modal (FOMO25/FOMO-MRI, gated)')
    b = fetch_fomo_multimodal(n_subjects=2)
    print()
    print('-> 3/5 Proprietary tumor DICOM (UniDataPro/brain-cancer-dataset)')
    c = fetch_unidata_dicom(n_per_series=2)
    print()
    print('-> 4/5 Multi-patient tumor (Ultralytics/Brain-tumor)')
    d = fetch_ultralytics_tumor_patients(n_patients=10)
    print()
    print('-> 5/5 Binary tumor (miladfa7 mirror of Navoneel Chakrabarty)')
    e = fetch_navoneel_binary(n=6)
    print()
    print('=== Summary ===')
    print(f'  OpenNeuro healthy:        {len(a)}')
    print(f'  FOMO multi-modal:         {len(b)}')
    print(f'  UniData tumor (1 pt):     {len(c)}')
    print(f'  Ultralytics ({len(d)} pts):       {len(d)}')
    print(f'  Navoneel binary:          {len(e)}')
    print(f'  elapsed: {time.perf_counter() - t0:.1f}s')


if __name__ == '__main__':
    main()