Tri-Netra-AI / scripts /fetch_ood_samples.py
anannyavyas1's picture
Upload folder using huggingface_hub
1cf3825 verified
Raw
History Blame Contribute Delete
15.5 kB
"""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()