Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """Build a combined brain parcellation atlas for mindVisualizer. | |
| Combines three complementary atlases (all in MNI152 1mm space) into a single | |
| NIfTI volume with a unified label map: | |
| Layer 1 — Harvard-Oxford Cortical (48 regions): | |
| Broad cortical coverage with LLM-friendly names. | |
| Layer 2 — Harvard-Oxford Subcortical (17 regions): | |
| Thalamus, putamen, caudate, hippocampus, amygdala, etc. | |
| Layer 3 — Julich-Brain cytoarchitectonic (62 regions, HIGHEST PRIORITY): | |
| Fine-grained motor (BA4a/4p), somatosensory (BA1-3), visual (V1-V5), | |
| auditory (TE1.0-1.2), Broca's (BA44/45), hippocampal subfields, | |
| amygdala subdivisions, white matter tracts. | |
| Where Julich-Brain has a label, it overrides the coarser Harvard-Oxford label. | |
| This gives maximum spatial coverage with maximum detail where available. | |
| Output: | |
| data/extra_parcellation/combined_atlas.nii.gz (NIfTI volume, ~700KB) | |
| data/extra_parcellation/combined_atlas_labels.json (label ID → name map) | |
| Requirements: | |
| pip install nilearn nibabel | |
| Usage: | |
| python scripts/setup_extra_parcellation.py | |
| """ | |
| import json | |
| import os | |
| import sys | |
| from pathlib import Path | |
| # Ensure project root is importable | |
| ROOT = Path(__file__).resolve().parent.parent | |
| OUT_DIR = ROOT / "data" / "extra_parcellation" | |
| def main(): | |
| try: | |
| import nibabel as nib | |
| import numpy as np | |
| except ImportError: | |
| print("ERROR: nibabel and numpy are required.") | |
| print(" pip install nibabel numpy") | |
| sys.exit(1) | |
| try: | |
| import nilearn.datasets as ds | |
| except ImportError: | |
| print("ERROR: nilearn is required to fetch the source atlases.") | |
| print(" pip install nilearn") | |
| sys.exit(1) | |
| OUT_DIR.mkdir(parents=True, exist_ok=True) | |
| out_nii = OUT_DIR / "combined_atlas.nii.gz" | |
| out_labels = OUT_DIR / "combined_atlas_labels.json" | |
| if out_nii.exists() and out_labels.exists(): | |
| print(f"[setup] Combined atlas already exists: {out_nii}") | |
| print("[setup] Delete it and re-run to rebuild.") | |
| return | |
| # ---- Fetch source atlases via nilearn (auto-downloads) ---- | |
| print("[setup] Fetching Harvard-Oxford cortical atlas ...") | |
| ho_cort = ds.fetch_atlas_harvard_oxford("cort-maxprob-thr25-1mm") | |
| print("[setup] Fetching Harvard-Oxford subcortical atlas ...") | |
| ho_sub = ds.fetch_atlas_harvard_oxford("sub-maxprob-thr25-1mm") | |
| print("[setup] Fetching Julich-Brain cytoarchitectonic atlas ...") | |
| juelich = ds.fetch_atlas_juelich("maxprob-thr25-1mm") | |
| # ---- Load NIfTI images ---- | |
| def _load(maps): | |
| return maps if hasattr(maps, "dataobj") else nib.load(maps) | |
| ho_cort_img = _load(ho_cort["maps"]) | |
| ho_sub_img = _load(ho_sub["maps"]) | |
| juelich_img = _load(juelich["maps"]) | |
| ho_cort_data = np.asarray(ho_cort_img.dataobj) | |
| ho_sub_data = np.asarray(ho_sub_img.dataobj) | |
| juelich_data = np.asarray(juelich_img.dataobj) | |
| assert ho_cort_data.shape == ho_sub_data.shape == juelich_data.shape, \ | |
| "Atlas shapes do not match — cannot combine" | |
| assert np.allclose(ho_cort_img.affine, juelich_img.affine), \ | |
| "Atlas affines do not match — not in the same MNI space" | |
| # ---- Combine: lowest priority first, highest last ---- | |
| combined = np.zeros(ho_cort_data.shape, dtype=np.int32) | |
| combined_labels = {} | |
| # Layer 1: Harvard-Oxford Cortical (label IDs 1–48) | |
| for i in range(1, len(ho_cort["labels"])): | |
| combined[ho_cort_data == i] = i | |
| combined_labels[str(i)] = str(ho_cort["labels"][i]) | |
| # Layer 2: Harvard-Oxford Subcortical (label IDs 100+) | |
| for i in range(1, len(ho_sub["labels"])): | |
| name = str(ho_sub["labels"][i]) | |
| # Skip overly broad labels | |
| if "Cortex" in name or "White Matter" in name: | |
| continue | |
| label_id = 100 + i | |
| combined[ho_sub_data == i] = label_id | |
| combined_labels[str(label_id)] = name | |
| # Layer 3: Julich-Brain (label IDs 200+, overwrites everything) | |
| for i in range(1, len(juelich["labels"])): | |
| label_id = 200 + i | |
| combined[juelich_data == i] = label_id | |
| name = str(juelich["labels"][i]) | |
| # Clean up prefixes | |
| if name.startswith("GM "): | |
| name = name[3:] | |
| elif name.startswith("WM "): | |
| name = "WM: " + name[3:] | |
| combined_labels[str(label_id)] = name | |
| # ---- Save ---- | |
| combined_img = nib.Nifti1Image(combined, ho_cort_img.affine) | |
| nib.save(combined_img, str(out_nii)) | |
| with open(out_labels, "w", encoding="utf-8") as f: | |
| json.dump(combined_labels, f, indent=2, ensure_ascii=False) | |
| n_labels = len(combined_labels) | |
| coverage = int((combined > 0).sum()) | |
| total = int(combined.size) | |
| pct = 100 * coverage / total | |
| print(f"\n[setup] Combined atlas saved:") | |
| print(f" NIfTI: {out_nii} ({os.path.getsize(out_nii):,} bytes)") | |
| print(f" Labels: {out_labels} ({n_labels} labels)") | |
| print(f" Coverage: {coverage:,} / {total:,} voxels ({pct:.1f}%)") | |
| print(f" Sources:") | |
| n_ho_c = sum(1 for k in combined_labels if 1 <= int(k) <= 99) | |
| n_ho_s = sum(1 for k in combined_labels if 100 <= int(k) <= 199) | |
| n_jue = sum(1 for k in combined_labels if 200 <= int(k) <= 299) | |
| print(f" Harvard-Oxford Cortical: {n_ho_c} labels") | |
| print(f" Harvard-Oxford Subcortical: {n_ho_s} labels") | |
| print(f" Julich-Brain: {n_jue} labels") | |
| if __name__ == "__main__": | |
| main() | |