Detecting and localizing cerebral aneurysms in digital subtraction angiography
English | 한국어 · Training guide
Warning
Research and educational project. Not a medical device. This system has not been clinically validated and must not be used for diagnosis or patient care.
A cerebral aneurysm is a localized dilation of an intracranial artery; rupture is frequently fatal, so detecting one on angiography matters clinically. This repository frames that detection as two coupled learning problems over anonymized digital subtraction angiography (DSA):
| Task | Output | |
|---|---|---|
| 1 | Detection — is an aneurysm present? | one real value in [0, 1] per patient |
| 2 | Localization — which arterial segments? | binary label for each of 21 segments |
Each patient contributes 8 angiographic views. Since no single view exposes every artery, images and labels are partitioned along the brain's anterior (internal carotid) and posterior (vertebral) circulation before training, producing four models that are combined by an explicit decision rule.
What this repository contains. The original system placed first (Grand Prize) in the 2023 K-ium medical AI competition, but its training code was not preserved — only the trained checkpoints and a written description. The code here is an open reimplementation reconstructed from that description: a complete, runnable pipeline covering data construction, preprocessing, training, evaluation, and challenge-format inference. Reproduction results are reported in Results; they do not currently match the original system.
Validation figures from the reimplementation, on a patient-grouped 80/20 split (seed 42, ~900 patients). AUROC is the competition's primary metric.
| Task | Model | Metric | Result | Random baseline |
|---|---|---|---|---|
| Detection — anterior | MedNet (ResNet-18) | AUROC | 0.677 | 0.500 |
| Detection — posterior | MedNet (ResNet-18) | AUROC | 0.490 | 0.500 |
| Localization — anterior | SwinV2-tiny | mAP | 0.071 | ~0.043 |
| Localization — posterior | ResNet-18 | mAP | 0.051 | ~0.013 |
How to read this. Anterior detection is the only task with a clear margin over chance (AUROC 0.677), and it remains weak — useful as a baseline, not as a decision aid. Posterior detection sits at chance: the model learned nothing usable. Both localization scores are low in absolute terms, though posterior is roughly 3.8× its random baseline and anterior about 1.7×, so some ranking signal exists. Accuracy is deliberately excluded from this table: with 1–4% positive label cells, predicting all-zeros scores 96–99% while being clinically worthless.
Reproduce these numbers with
scripts/evaluate.py; see TRAINING.md. Nothing in this table is estimated.
Three factors are identifiable rather than speculative:
- Severe class imbalance. Positive views: 22.8% anterior, 7.2% posterior for detection; 4.1% and 1.2% of label cells for localization. Posterior aneurysms are both rarer and smaller.
- Label noise from view geometry. Contrast injected on one side often opacifies the other, so a patient-level positive label may describe an aneurysm not visible in the view it is attached to.
- Lost preprocessing detail. The original text/margin-removal coordinates were not recorded; the reconstruction infers them, and residual annotation artifacts are plausible confounders.
See Current status for what is being addressed.
Brain circulation splits into anterior (internal carotid, ICA) and posterior (vertebral, VA) systems, and labels are grouped to match:
| Circulation | Backbone vessel | Segments |
|---|---|---|
| Anterior | ICA | ICA, AntChor, ACA, ACOM, MCA |
| Posterior | VA | VA, PICA, SCA, BA, PCA, PCOM |
This yields four datasets — detection and localization, each split anterior and posterior.
train.csv holds one row per patient: Index, Aneurysm (0/1), and 21
location columns. Each patient has 8 images, {Index}{SIDE}{VESSEL}-{ANGLE}.jpg:
| Suffix | Injection | Circulation | Labelled from |
|---|---|---|---|
LI-A, LI-B |
left internal carotid | anterior | L_* anterior columns |
RI-A, RI-B |
right internal carotid | anterior | R_* anterior columns |
LV-A, LV-B |
left vertebral | posterior | L_* posterior columns |
RV-A, RV-B |
right vertebral | posterior | R_* posterior columns |
A and B are two projection angles. BA (basilar artery) is midline with no
L/R variant and is shared by both posterior sides — which is why 5 anterior + 6
posterior labels map onto 21 CSV columns rather than 22.
One patient row therefore expands to 4 anterior + 4 posterior training rows. Left/right are merged at the label level and resolved again at inference.
The dataset is private, anonymized medical imaging and is not distributed here.
Training images arrive in three forms, handled separately:
| Form | Handling |
|---|---|
| No margin, no text | used unchanged |
| Text + margin | margin removed by pixel value; the fixed text region is masked and filled with mean background sampled from an adjacent patch |
| Margin only | margin width varies per image, so horizontal and vertical center lines are scanned for the gray boundary and the image is cropped there |
Training-time augmentation applies rotation (±10°), translation, scaling, and brightness/contrast jitter. Horizontal flipping is deliberately excluded — laterality is anatomically meaningful, and each view is labelled with its own side's columns, so mirroring would contradict the label.
| Task | Backbone | Initialization | Loss |
|---|---|---|---|
| Detection (both) | ResNet-18 | MedicalNet (Med3D) | BCEWithLogitsLoss, positive-weighted |
| Localization — anterior | swinv2_cr_tiny_ns_224 (timm) |
ImageNet | Asymmetric Loss |
| Localization — posterior | ResNet-18 (torchvision) | ImageNet | Asymmetric Loss |
MedicalNet ships 3D kernels ([out, in, D, H, W]) from volumetric
pretraining. src/models.py deflates them to 2D by averaging over the depth
axis — the inverse of I3D inflation — and adapts the stem to single-channel
input before loading, so the grayscale medical pretraining is actually used.
Coverage is reported at load time (~84%; the checkpoint contains no shortcut
projections), and if it falls below a threshold the builder falls back to
ImageNet rather than silently training from scratch.
- Detect. Views scoring above the binary threshold are treated as aneurysm-bearing; all others have every location label forced to 0.
- Localize. Positive views run through the location model; each segment is thresholded at its own 90th percentile of predicted scores.
- Aggregate. Anterior and posterior scores are averaged over their 4 views
each; the larger average becomes the patient-level score. Per-view location
predictions map back to
L_/R_columns by originating view, withBApooled across all posterior views.
git clone https://github.com/pmy02/Cerebral_Aneurysm_Classification.git
cd Cerebral_Aneurysm_Classification
git lfs install && git lfs pull # fetch checkpoints
conda create -n aneurysm python=3.10 -y && conda activate aneurysm
pip install torch torchvision # macOS; use the CUDA index on Linux
pip install -r requirements.txtpython -m scripts.check_env # verify CUDA / Apple MPS
python -m scripts.check_data --csv data/train_set/train.csv --image-root data/train_set
python -m scripts.train --config configs/binary_anterior.yaml \
--csv data/train_set/train.csv --image-root data/train_set
python -m scripts.evaluate --config configs/binary_anterior.yaml \
--checkpoint Model/MedNet_ant_binary.pt \
--csv data/train_set/train.csv --image-root data/train_set
python -m scripts.predict --test-csv data/test_set/test.csv \
--image-root data/test_set --out output.csvTraining runs on CUDA, Apple Silicon (MPS), or CPU — detected automatically. On
a Mac use the lighter configs/mac/*.yaml presets.
Full walkthrough: TRAINING.md · 한국어 — setup, data layout, per-model commands, runtimes, troubleshooting.
Cerebral_Aneurysm_Classification/
├── src/
│ ├── data.py # CSV → per-view frames, patient split, transforms
│ ├── preprocessing.py # margin / text removal and cropping
│ ├── models.py # MedNet (Med3D→2D), SwinV2, ResNet-18 builders
│ ├── losses.py # Asymmetric Loss
│ ├── metrics.py # AUROC, F1, mAP, per-class support
│ ├── checkpoint.py # weight loading with layer-match reporting
│ └── device.py # CUDA / MPS / CPU selection
├── scripts/
│ ├── check_env.py # verify GPU acceleration
│ ├── check_data.py # pre-flight dataset validation
│ ├── train.py # config-driven training with early stopping
│ ├── evaluate.py # checkpoint → metrics table
│ ├── predict.py # test set → challenge-format output.csv
│ ├── infer.py # decision-rule helpers
│ └── gradcam.py # Grad-CAM heatmaps
├── configs/ # one YAML per task × circulation
│ └── mac/ # lighter Apple Silicon presets
├── Model/ # checkpoints (Git LFS)
├── TRAINING.md # local training guide
└── README.md
Working
- End-to-end pipeline: data construction → training → evaluation → submission CSV
- Patient-grouped splitting (no view-level leakage between train and validation)
- MedicalNet 3D→2D transfer with explicit coverage reporting
- Cross-platform training (CUDA / Apple MPS / CPU)
Known limitations
- Posterior detection performs at chance; not usable as-is
- Localization is weak in absolute terms for both circulations
- Rare segments (e.g.
SCA,PCOM,PCA) can have zero positive examples in a validation split; those are excluded from macro averages and flagged rather than silently averaged in - Preprocessing coordinates are inferred, not recovered from the original
Next steps
- Patient-level cross-validation instead of a single split, so rare segments are evaluable
- Loss and sampling strategies targeted at the posterior imbalance
- Recovering or re-deriving the original preprocessing parameters
Grad-CAM was applied to text- and margin-stripped images without augmentation to inspect which regions drove predictions. Heatmaps were interpretable, but were used for qualitative inspection rather than rigorous attribution, since they were not calibrated against ground-truth localization accuracy.
Approaches tried, including those that did not work:
| Approach | Outcome |
|---|---|
| Rule-based cropping around positive segments | Abandoned — two aneurysms in one segment still yield a single 1, so exact positions are unrecoverable |
| Splitting by position / angle / direction, with weighted sampling | Imbalance dominated; weighting gave marginal gains |
| Self-supervised pretraining (autoencoder, 200 epochs) | Imbalance still limited downstream supervised performance |
| Contrastive learning over ResNet-50 features | Same limitation |
ResNet-18 with fc→conv head (preserve spatial information) |
F1 flat at 53.68% across epochs |
| DenseNet for stronger feature extraction | No decisive improvement |
Minyoung Park · LinkedIn · minyo0119@gmail.com




