CTP Core / Penumbra Segmentation
3D nnU-Net · admission 4D CT perfusion · IEEE BHI 2026
Twelve consecutive slices through one held-out case. Fills are the model's prediction, outlines are the expert annotation.
If you use these weights, please cite the paper. Md Sazidur Rahman, Kjersti Engan, Kathinka Dæhli Kurz and Mahdieh Khanmohammadi, Bi-temporal Image-driven Acute Stroke Evolution Analysis, IEEE-EMBS International Conference on Biomedical and Health Informatics (BHI), 2026. arXiv:2602.07535 · BibTeX in Citation below.
Segments the ischemic core and the penumbra from admission 4D CT perfusion. The 40 CTP timepoints go in as input channels, so the network sees the whole bolus passage rather than derived perfusion maps.
| Task | 4-class segmentation, background / brain / penumbra / core |
| Input | 40-channel CTP volume in NCCT space |
| Framework | nnU-Net v2, 3d_fullres, 5-fold ensemble |
| Trained on | 116 cases, Stavanger University Hospital |
| Dice | penumbra 0.71, core 0.30 (held-out, n = 33) |
On the linked dataset. This model is trained on the SUH cohort only. The linked ISLES'24 dataset is where the model was applied, not trained or scored: ISLES'24 has no manual admission core/penumbra annotation, so the model supplies those labels and no Dice can be computed against it.
Try it in the browser
Live demo — a ZeroGPU Space that runs these weights on one bundled ISLES'24 case and then intersects the result with the follow-up infarct to give the paper's six outcome classes. It also accepts an uploaded 40-channel study. The five-fold ensemble takes about 15 s of GPU time; preprocessing for the bundled case is precomputed.
The demo/ folder in this repository holds that case: demo_case.npz is the
40-channel CTP already preprocessed onto the model's grid, and
demo_labels.npz the follow-up infarct and contralateral-brain masks. Both are
bare voxel arrays — no affine, no NIfTI header, no DICOM tags, no case
identifier. They live here rather than in the Space because a free Space is
capped at 1 GB and keeps every revision of an LFS file.
Quick start
The simplest route is the bundled script, which fetches the weights, points nnU-Net at them and checks your input satisfies the 40-channel contract before spending an hour on inference:
pip install nnunetv2 huggingface_hub
hf download yokko123/ctp-core-penumbra-nnunet predict.py --local-dir .
python predict.py -i INPUT_FOLDER -o OUTPUT_FOLDER
python predict.py -i INPUT_FOLDER -o OUTPUT_FOLDER --folds 0 --disable-tta # fast
python predict.py -i INPUT_FOLDER -o OUTPUT_FOLDER --postprocess
Or drive nnU-Net directly:
hf download yokko123/ctp-core-penumbra-nnunet --local-dir $nnUNet_results
nnUNetv2_predict \
-d Dataset1152_SUS_CTP_Reg_3DT \
-i INPUT_FOLDER -o OUTPUT_FOLDER \
-f 0 1 2 3 4 -tr nnUNetTrainer -c 3d_fullres -p nnUNetPlans
For a reproducible fetch, the code repository pins this model by commit sha in
models.yaml
and downloads it with python download_weights.py --model fe4_nnunet, which
also verifies every file against its expected size.
Optional connected-component postprocessing, with the rules nnU-Net chose on the cross-validation:
nnUNetv2_apply_postprocessing \
-i OUTPUT_FOLDER -o OUTPUT_FOLDER_PP \
-pp_pkl_file $nnUNet_results/Dataset1152_SUS_CTP_Reg_3DT/nnUNetTrainer__nnUNetPlans__3d_fullres/postprocessing.pkl \
-plans_json $nnUNet_results/Dataset1152_SUS_CTP_Reg_3DT/nnUNetTrainer__nnUNetPlans__3d_fullres/plans.json
Weight formats
Each fold ships twice.
| file | size | contents |
|---|---|---|
…/fold_N/checkpoint_final.pth |
357 MB | the nnU-Net training checkpoint: weights, optimiser state, AMP scaler, epoch log, plans |
…/safetensors/fold_N.safetensors |
178 MB | the weights alone |
model.safetensors |
178 MB | a copy of fold 0 at the repo root |
model.safetensors is there only so the Hub can read a parameter count and
show 44.6M params in the sidebar — it scans that one path and ignores
nested ones. It is fold 0, not an ensemble and not a merged model; predictions
in the paper use all five folds.
The .pth files are what nnU-Net loads, so the commands above need nothing
else. The safetensors copies exist because a .pth is a Python pickle and has
to be trusted before it can even be opened, and because half of each one is
optimiser state that inference never touches.
Both carry the same 44,590,840 parameters, bit for bit. Note that nnU-Net's
state dict has 344 entries for those parameters: each conv block exposes its
layers twice (convs.N.conv.weight and convs.N.all_modules.M.weight) and the
decoder holds a reference to the encoder, so most tensors appear four times
under different names. torch deduplicates by storage when saving; safetensors
refuses aliased tensors outright. The export therefore writes 128 distinct
tensors and records the alias names and the original key order in the
safetensors metadata header, next to trainer_name, configuration and
inference_allowed_mirroring_axes.
tools/safetensors_to_nnunet.py replays that. Either rebuild inference-only
checkpoints and carry on with stock nnU-Net:
hf download yokko123/ctp-core-penumbra-nnunet --local-dir $nnUNet_results \
--exclude "*/fold_*/checkpoint_final.pth" --exclude "demo/*" \
--exclude "model.safetensors"
python tools/safetensors_to_nnunet.py \
$nnUNet_results/Dataset1152_SUS_CTP_Reg_3DT/nnUNetTrainer__nnUNetPlans__3d_fullres
or load the weights straight into a predictor:
from tools.safetensors_to_nnunet import load_fold
predictor.list_of_parameters = [
load_fold(f"{trainer_dir}/safetensors/fold_{f}.safetensors") for f in range(5)
]
The rebuilt checkpoints keep the weights and the three fields nnU-Net reads at
inference time; the optimiser state and the training log are gone, which is why
they are half the size of the originals. tools/export_safetensors.py is the
exporter, included so the conversion is reproducible rather than something you
have to take on faith.
Input contract
All 40 channels are declared NoNormalization, so the model expects exactly the
preprocessing it was trained on and will not behave sensibly on raw CTP:
- 4D CTP motion-corrected by rigidly registering every frame to the first,
- temporally resampled to a uniform 1 fps, giving 40 frames,
- rigidly registered to the admission NCCT and resampled onto that grid,
- skull-stripped (SynthStrip),
- windowed to [0, 100] HU and rescaled to an 8-bit grey range.
One case is 40 files, CASE_0000.nii.gz … CASE_0039.nii.gz, in timepoint
order. The full chain that produced these inputs is stage 01 of the
code repository.
Labels
| Value | Class |
|---|---|
| 0 | background |
| 1 | brain |
| 2 | penumbra |
| 3 | core |
Results
Five-fold cross-validation over the 116 development cases:
| Class | Dice | IoU |
|---|---|---|
| brain | 0.975 | 0.952 |
| penumbra | 0.602 | 0.488 |
| core | 0.297 | 0.207 |
| foreground mean | 0.624 |
Held-out test set, 33 cases, as reported in the paper:
| Class | Dice |
|---|---|
| penumbra | 0.71 ± 0.24 |
| core | 0.30 ± 0.28 |
Core is much harder than penumbra. Core regions are small, their CTP boundary is genuinely ambiguous, and a handful of voxels moves Dice a long way. Treat core output as approximate localisation, not a volume measurement.
Architecture
| Trainer / plans | nnUNetTrainer / nnUNetPlans |
| Network | PlainConvUNet, 7 stages, features 32 → 320 |
| Patch size | 20 × 320 × 256 |
| Batch size | 2 |
| Target spacing | 3.0 × 0.415 × 0.415 mm |
| Input channels | 40, all NoNormalization |
Trained with lr 5·10⁻³, weight decay 10⁻⁴, 250 train / 50 validation iterations per epoch, 85 % foreground oversampling and deep supervision, on a 500-epoch budget.
The checkpoint_final shipped per fold sits at epoch 328, 534, 430, 154 and 147
for folds 0 to 4, so the folds are not equally trained. The figures above were
computed from exactly these checkpoints and already reflect that.
Role in the paper
These are the FE4 weights in:
Bi-temporal Image-driven Acute Stroke Evolution Analysis Md Sazidur Rahman, Kjersti Engan, Kathinka Dæhli Kurz, Mahdieh Khanmohammadi IEEE-EMBS International Conference on Biomedical and Health Informatics (BHI), 2026 arXiv:2602.07535
The model does two jobs there: it produces the admission core and penumbra
labels for the ISLES'24 cohort, which has no manual T₁ annotation, and its
stage-3 encoder features are read out as a 256-D tissue descriptor. For the
feature read-out rather than the segmentation, use
02_features/fe4_nnunet/extract_3D_ensemble_features.py from the code
repository.
Companion model
The FE3 encoder from the same paper is released separately as yokko123/ctp-core-penumbra-mjnet — a 0.98 M-parameter mJ-Net. It segments far worse than this model (penumbra Dice 0.30, core 0.05) but its 256-D bottleneck gave the paper's strongest separation between salvaged and infarcted penumbra.
Training data
A retrospective Stavanger University Hospital (SUH) cohort: patients scanned between January 2014 and August 2020 with admission CTP and follow-up MRI, 149 in total, split 116 for development and 33 held out. Core and penumbra were annotated by expert neuroradiologists on the CTP-derived perfusion maps (CBF, CBV, Tmax) and MIP images.
The cohort is not public and is not distributed with these weights.
Citation
@inproceedings{rahman2026bitemporal,
title = {Bi-temporal Image-driven Acute Stroke Evolution Analysis},
author = {Rahman, Md Sazidur and Engan, Kjersti and
Kurz, Kathinka D{\ae}hli and Khanmohammadi, Mahdieh},
booktitle = {2026 IEEE-EMBS International Conference on Biomedical and Health Informatics (BHI)},
year = {2026},
eprint = {2602.07535},
archivePrefix= {arXiv},
primaryClass = {cs.CV},
note = {In press}
}
Built with nnU-Net (Isensee et al., 2021), which is Apache-2.0.