Official code repository for the MICCAI BraTS-Path 2026 challenge submission:
"Rare-Class Conditional Routing with Foundation Model Representations for Glioma Histologic Sub-region Classification"
Shubham Innani, Suhang You, Carla Pitarch-Abaigar, Dimitrios Makris, Spyridon Bakas
We systematically evaluate fully-supervised and semi-supervised training paradigms on top of four pathology foundation model backbones for 10-class H&E patch classification of glioma sub-regions. Our best system (BraTS-Path Score = 0.750, MCC = 0.858, F1 = 0.641) uses a class-conditional routing ensemble: LoRA-UNI2 handles rare classes {DM, LI, PL} and XGBoost-UNI2 handles all remaining classes.
| Method | Backbone | MCC | F1 | BPS |
|---|---|---|---|---|
| XGBoost | UNI2 | 0.847 | 0.475 | 0.661 |
| LoRA | UNI2 | 0.822 | 0.580 | 0.701 |
| CDMA (semi-sup) | H-Optimus-1 | 0.800 | 0.573 | 0.687 |
| XGBoost ensemble (3 models) | UNI2+Virchow2+H-Opt1 | 0.865 | 0.500 | 0.683 |
| LoRA ensemble (3 models) | UNI2+Virchow2+H-Opt1 | 0.838 | 0.607 | 0.723 |
| Routing ensemble (Config 02) | LoRA-UNI2 + XGB-UNI2 | 0.858 | 0.641 | 0.750 |
.
├── src/ # Training and evaluation code
│ ├── main_v2.py # Supervised MLP (frozen backbone + head)
│ ├── main_lora.py # Supervised LoRA fine-tuning
│ ├── main_cdma.py # Semi-supervised CDMA (3-head cross-distillation)
│ ├── main_cdma_lora.py # Semi-supervised CDMA + LoRA
│ ├── extract_embeddings.py # Extract frozen FM embeddings (labeled set)
│ ├── extract_unlabelled_emb.py # Extract FM embeddings (unlabeled WSI patches)
│ ├── train_xgb.py # XGBoost training with chunking + rare-boost
│ ├── pseudo_label_xgb.py # Centroid-based pseudo-labeling for XGBoost
│ ├── build_ensembles.py # Class-routing ensemble construction
│ └── predict.py # Inference / prediction CSV export
│
├── docker/ # Docker submission (Config 02 — best system)
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── run.py # Challenge entrypoint (stable, do not modify)
│ ├── src/
│ │ ├── inference.py # High-level inference flow
│ │ ├── inference_dependencies.py # Model loading, embedding, routing logic
│ │ └── webdataset_loader.py
│ └── scripts/
│ ├── 01_build_image.sh # Build Docker image + save .tar archive
│ ├── 02_run_docker_image.sh # Run built image on local data
│ └── 03_convert_tar_to_sif.sh # Convert to Apptainer SIF
│
├── train.sh # SLURM dispatch for all training jobs
└── BraTS-Path-2026-Train-Patch-Patient-Slide-Mapping.csv
Challenge page (Task 5): https://challenges.synapse.org/Challenges/DetailsPage/Task5?id=syn74274097
You must register for the challenge and accept the data use agreement on Synapse before data access is granted.
pip install synapseclient
synapse login --authToken YOUR_SYNAPSE_PATGet a Personal Access Token at: https://www.synapse.org/#!PersonalAccessTokens:
All data is distributed as WebDataset .tar shards. Each shard contains .jpg patch images (512×512 px, 20×, JPEG quality 90) and .cls label files (integer 0–9).
# Labeled training shards (~1.6M patches, 40 shards)
synapse get -r syn74274097 --include "BraTS-Path2026-Train-TARS*" --downloadLocation data/train_shards/
# Unlabeled WSI large images (.tiff + foreground masks, 80 WSIs)
# Scripts for patching these into shards are provided by the organizers on Synapse.
synapse get -r syn74274097 --include "*Unlabeled*" --downloadLocation data/unlabeled/
# Validation shards (~114K patches, no labels released)
synapse get -r syn74274097 --include "BraTS-Path2026-Valid*" --downloadLocation data/val_shards/A small debug set (3,200 patches randomly sampled from training) is available without registration at: https://www.synapse.org/Synapse:syn74488419
| Label | Class |
|---|---|
| 0 | CT — Cellular Tumor |
| 1 | DM — Dense Macrophages |
| 2 | IC — Infiltration into Cortex |
| 3 | LI — Leptomeningeal Infiltration |
| 4 | MP — Microvascular Proliferation |
| 5 | NC — Geographic Necrosis |
| 6 | PL — Presence of Lymphocytes |
| 7 | PN — Pseudopalisading Necrosis |
| 8 | WM — Penetration into White Matter |
| 9 | NOTA — None of the Above |
data/
├── train_shards/
│ └── shard-{000000..000039}.tar # labeled training shards
├── unlabeled/
│ └── *.tiff # unlabeled WSIs for semi-supervised use
└── val_shards/
└── *.tar # validation shards (Docker inference input)
The BraTS-Path-2026-Train-Patch-Patient-Slide-Mapping.csv file in this repo maps each training patch name to its patient ID (0–125) and slide ID (0–254), which we used for patient-stratified splits.
import webdataset as wds
from torchvision import transforms
transform = transforms.Compose([transforms.Resize((224, 224)), transforms.ToTensor()])
dataset = (
wds.WebDataset("data/train_shards/shard-{000000..000039}.tar", shardshuffle=True)
.shuffle(5000)
.decode("pil")
.to_tuple("__key__", "jpg", "cls")
.map_tuple(
lambda k: k.decode() if isinstance(k, bytes) else k,
transform,
lambda c: int(c.decode() if isinstance(c, bytes) else c),
)
)Note: Challenge data is subject to a data use agreement. Do not redistribute.
conda create -n bratspath python=3.10
conda activate bratspath
pip install torch==2.3.1 torchvision==0.18.1 --index-url https://download.pytorch.org/whl/cu121
pip install timm==1.0.12 scikit-learn xgboost peft huggingface_hub \
webdataset tqdm tensorboard transformers numpyWe evaluate four pathology foundation model backbones:
| Backbone | Dim | Source |
|---|---|---|
| H-Optimus-1 | 1536 | Bioptimus (HuggingFace: bioptimus/H-optimus-1) |
| UNI2-h | 1536 | MahmoodLab (HuggingFace: MahmoodLab/UNI2-h) |
| Virchow2 | 2560 | Paige (HuggingFace: paige-ai/Virchow2) |
| DINOv3-GBM | 1024 | In-house, pretrained on TCGA-GBM/LGG (~2K WSIs) |
All pan-pathology models download automatically at first run. For dinov3_custom, place weights at:
dinov3_weights/gbm_lgg_all/clean_dino_state_dict.pth
Extract frozen embeddings for XGBoost training:
# Labeled training set
python src/extract_embeddings.py \
--model uni2 \
--data_dir /path/to/BraTS-Path2026-Train-JPG \
--output_dir embeddings/uni2
# Unlabeled WSI patches (for pseudo-labeling / CDMA)
python src/extract_unlabelled_emb.py \
--model uni2 \
--unlabeled_dir /path/to/unlabeled_patches \
--output_dir embeddings/uni2_unlabeledpython src/main_v2.py --model uni2 --epochs 100 --seed 42python src/main_lora.py \
--model uni2 \
--use_lora --lora_rank 4 --lora_alpha 8 \
--epochs 100 --min_epochs 50 --patience 15 \
--lr 5e-5 --backbone_lr 5e-6 \
--seed 42CDMA uses 649,670 unlabeled patches extracted from the 80 provided WSIs. Three identical MLP heads operate on the backbone embedding; multiplicative noise U[−0.3, 0.3] is applied to heads 2 and 3 during training. Cross-knowledge distillation via symmetric pairwise KL divergence (T=10) runs over all six ordered head pairs on both labeled and unlabeled samples.
python src/main_cdma.py \
--model uni2 \
--unlabeled_dir /path/to/unlabeled_patches \
--seed 42python src/main_cdma_lora.py \
--model uni2 \
--unlabeled_dir /path/to/unlabeled_patches \
--lora_rank 4 --lora_alpha 8 \
--seed 42Embeddings must be extracted first (see Feature Extraction above).
The training set is split into 10 stratified chunks. Rare classes (count < 10,000: DM, LI, PL) are replicated into every chunk so all 10 models see rare-class examples. Per-chunk class weights are set to inverse class frequency. After averaging 10-chunk probabilities, a rare-boost multiplier (max_count / count_c)^0.3 is applied and renormalized. Per-class thresholds are then calibrated by coordinate ascent (3 rounds, 30 grid points in [0.2, 5.0]) to maximize macro-MCC.
python src/train_xgb.py \
--embeddings_dir embeddings/uni2 \
--output_dir output_xgb/uni2 \
--n_chunks 10Assigns pseudo-labels to unlabeled patches based on cosine distance to per-class centroids of training embeddings. Evaluated at the 35th, 60th, and 80th confidence percentiles. Note: this was a negative result — all thresholds degraded XGBoost performance relative to the supervised baseline.
python src/pseudo_label_xgb.py \
--embeddings_dir embeddings/uni2 \
--unlabeled_embeddings embeddings/uni2_unlabeled \
--output_dir output_pseudo_label \
--percentile 35 # or 60, 80Routes rare classes {DM=1, LI=3, PL=6} to LoRA-UNI2 and all other classes to XGBoost-UNI2. If either model predicts a rare class for a patch, the LoRA prediction is used.
python src/build_ensembles.py \
--lora_preds output_lora/uni2/predictions.csv \
--xgb_preds output_xgb/uni2/predictions.csv \
--output submissions/ensemble02_predictions.csvFull routing sweep (all 12 configurations):
python src/build_ensembles.py --sweeptrain.sh dispatches to the correct script based on three positional arguments: <model> <lora|nolora> <supervised|semi>
sbatch --job-name=uni2_lora train.sh uni2 lora supervised
sbatch --job-name=uni2_cdma train.sh uni2 nolora semi
sbatch --job-name=hopt_lora train.sh h_optimus_1 lora supervisedThe Docker image packages Config 02 (LoRA-UNI2 + XGB-UNI2 routing ensemble) for offline inference on WebDataset .tar shards.
- Input: WebDataset
.tarshards mounted at/input - Output:
predictions.csvat/output - Pipeline: extract UNI2 embeddings → XGBoost probabilities → LoRA classifier logits → rare-class routing → write CSV
cd docker
IMAGE_NAME=brats_ensemble02 IMAGE_TAG=latest ./scripts/01_build_image.shThe build saves docker_archives/brats_ensemble02_latest.tar (~6.4 GB).
INPUT_DIR=/path/to/input_shards \
OUTPUT_DIR=/path/to/output \
IMAGE_NAME=brats_ensemble02 IMAGE_TAG=latest \
./scripts/02_run_docker_image.shTo override batch size (default 1536):
docker run --gpus all -e BATCH_SIZE=1024 ...IMAGE_NAME=brats_ensemble02 IMAGE_TAG=latest ./scripts/03_convert_tar_to_sif.sh
apptainer exec --nv \
--bind /path/to/input:/input:ro \
--bind /path/to/output:/output \
sif/brats_ensemble02_latest.sif \
python run.py --input /input --output /outputModel weights are not included in this repository (.pth files are git-ignored). Place them before building:
docker/src/ckpts/
lora_uni2_best.pth # LoRA-UNI2 classifier checkpoint
xgb_chunk_models.pkl # XGBoost 10-model ensemble
xgb_thresholds.npy # Per-class decision thresholds
docker/src/foundation_model_weights/
uni2_model.safetensors # UNI2-h backbone weights (offline copy)
| Parameter | Value |
|---|---|
| Rank | 4 |
| Alpha | 8 |
| Dropout | 0.05 |
| Head LR | 5×10⁻⁵ |
| Backbone LR | 5×10⁻⁶ |
| Schedule | OneCycleLR, 5% warm-up |
| Min epochs | 50 |
| Early stopping patience | 15 |
| Weight decay | 0.05 |
| Label smoothing | 0.1 |
| Seed | 42 |
| Parameter | Value |
|---|---|
| n_estimators | 300 |
| max_depth | 6 |
| learning_rate | 0.05 |
| subsample | 0.8 |
| colsample_bytree | 0.4 |
| min_child_weight | 5 |
| gamma | 0.1 |
| objective | multi:softprob |
| Rare-boost exponent | 0.3 |
| Threshold calibration | Coordinate ascent, 3 rounds, 30 pts/class in [0.2, 5.0] |
| Parameter | Value |
|---|---|
| Heads | 3 × (Linear→GELU→Dropout(0.5)→Linear) |
| Noise (heads 2,3) | U[−0.3, 0.3] multiplicative |
| KD temperature | 10 |
| Contrastive temperature | 0.07 |
| Projection head | embed → 256 → 128 |
| Epochs | 50 |
| Seed | 42 |
If you use this code, please cite:
@inproceedings{innani2026bratspath,
title = {Rare-Class Conditional Routing with Foundation Model Representations
for Glioma Histologic Sub-region Classification},
author = {Innani, Shubham and You, Suhang and Pitarch-Abaigar, Carla
and Makris, Dimitrios and Bakas, Spyridon},
booktitle = {MICCAI BraTS-Path 2026},
year = {2026}
}