diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a3ed70e..de11514 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,6 +26,24 @@ jobs:
- name: Test
run: uv run pytest
+ wheel:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: astral-sh/setup-uv@v10.2.0
+ - name: Build and install the wheel outside the repository
+ run: |
+ uv build --wheel -o /tmp/dist
+ uv venv /tmp/venv
+ VIRTUAL_ENV=/tmp/venv uv pip install /tmp/dist/*.whl --extra-index-url https://download.pytorch.org/whl/cpu --index-strategy unsafe-best-match
+ - name: Run the commands from another folder
+ working-directory: /tmp
+ run: |
+ mkdir -p data/P0001 && echo '{}' > data/P0001/config.json
+ /tmp/venv/bin/deepaneseg-train --help
+ /tmp/venv/bin/deepaneseg-preprocess data_dir=data
+ test -f data/0Work/split_pats.json
+
docker:
runs-on: ubuntu-latest
steps:
@@ -35,4 +53,4 @@ jobs:
- name: Smoke test
run: |
docker run --rm deepaneseg:cpu python -c "import torch, deepaneseg.utils; print(torch.__version__)"
- docker run --rm deepaneseg:cpu python scripts/train.py --help
+ docker run --rm deepaneseg:cpu deepaneseg-train --help
diff --git a/Dockerfile b/Dockerfile
index 5a05243..427424a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -21,4 +21,4 @@ RUN uv sync --locked --no-install-project --no-dev --extra ${TORCH} && rm -rf /r
COPY . .
RUN uv sync --locked --no-dev --extra ${TORCH} && rm -rf /root/.cache/uv
-CMD ["python", "scripts/train.py", "--help"]
+CMD ["deepaneseg-train", "--help"]
diff --git a/README.md b/README.md
index 2e6a625..e30b9b5 100644
--- a/README.md
+++ b/README.md
@@ -1,138 +1,211 @@
> **Note:** This repository is a copy of the original code hosted at [gitlab.inria.fr/yassis/DeepAneSeg](https://gitlab.inria.fr/yassis/DeepAneSeg), developed during my PhD at Inria/LORIA.
-# An Efficient Data Strategy for the Detection of Brain Aneurysms from MRA with Deep Learning
-This repository contains the official software code related to the paper ["An Efficient Data Strategy for the Detection of Brain Aneurysms from MRA with Deep Learning"](https://link.springer.com/chapter/10.1007/978-3-030-88210-5_22). The paper was published in MICCAI Workshop on Data Augmentation, Labelling, and Imperfections 2021.
-
-# Abstract
-The detection of intracranial aneurysms from Magnetic Resonance Angiography images is a problem of rapidly growing clinical importance. In the last 3 years, the raise of deep convolutional neural networks has instigated a streak of methods that have shown promising performance. The major issue to address is the very severe class imbalance. Previous authors have focused their efforts on the network architecture and loss function. This paper tackles the data. A rough but fast annotation is considered: each aneurysm is approximated by a sphere defined by two points. Second, a small patch approach is taken so as to increase the number of samples. Third, samples are generated by a combination of data selection (negative patches are centered half on blood vessels and half on parenchyma) and data synthesis (patches containing an aneurysm are duplicated and deformed by a 3D spline transform). This strategy is applied to train a 3D U-net model, with a binary cross entropy loss, on a data set of 111 patients (155 aneurysms, mean size 3.86 mm ± 2.39 mm, min 1.23 mm, max 19.63 mm). A 5-fold cross-validation evaluation provides state of the art results (sensitivity 0.72, false positive count 0.14, as per ADAM challenge criteria). The study also reports a comparison with the focal loss, and Cohen’s Kappa coefficient is shown to be a better metric than Dice for this highly unbalanced detection problem.
-
+# DeepAneSeg
+
+[](https://github.com/youssefassis/DeepAneSeg/actions/workflows/ci.yml)
+
+Detection of intracranial aneurysms in Time-of-Flight MRA with a 3D U-Net trained on small patches. This is the
+code of the paper [An Efficient Data Strategy for the Detection of Brain Aneurysms from MRA with Deep
+Learning](https://link.springer.com/chapter/10.1007/978-3-030-88210-5_22) (MICCAI 2021 DALI workshop), packaged as a
+set of commands that go from raw scans to detections and evaluation metrics.
+
+## Method
+The main difficulty is the extreme class imbalance: aneurysms occupy a tiny fraction of a scan. Rather than a new
+architecture or loss, the paper proposes a data strategy:
+- **Fast annotation:** each aneurysm is approximated by a sphere, defined by two points on a diameter.
+- **Small patches**, to increase the number of training samples.
+- **Sample selection and synthesis:** negative patches are centered half on blood vessels and half in the parenchyma,
+ and patches containing an aneurysm are duplicated and deformed by a random 3D spline transform.
+
+Trained with a binary cross-entropy loss on 111 patients (155 aneurysms), a 5-fold cross-validation reached a
+sensitivity of 0.72 with 0.14 false positives per patient (ADAM challenge criteria).
+
+
+Abstract
+
+The detection of intracranial aneurysms from Magnetic Resonance Angiography images is a problem of rapidly growing
+clinical importance. In the last 3 years, the raise of deep convolutional neural networks has instigated a streak of
+methods that have shown promising performance. The major issue to address is the very severe class imbalance.
+Previous authors have focused their efforts on the network architecture and loss function. This paper tackles the
+data. A rough but fast annotation is considered: each aneurysm is approximated by a sphere defined by two points.
+Second, a small patch approach is taken so as to increase the number of samples. Third, samples are generated by a
+combination of data selection (negative patches are centered half on blood vessels and half on parenchyma) and data
+synthesis (patches containing an aneurysm are duplicated and deformed by a 3D spline transform). This strategy is
+applied to train a 3D U-net model, with a binary cross entropy loss, on a data set of 111 patients (155 aneurysms,
+mean size 3.86 mm ± 2.39 mm, min 1.23 mm, max 19.63 mm). A 5-fold cross-validation evaluation provides state of the
+art results (sensitivity 0.72, false positive count 0.14, as per ADAM challenge criteria). The study also reports a
+comparison with the focal loss, and Cohen's Kappa coefficient is shown to be a better metric than Dice for this
+highly unbalanced detection problem.
+
+
+## Quick start
+```
+git clone https://github.com/youssefassis/DeepAneSeg && cd DeepAneSeg
+uv sync --extra cu126 # or --extra cpu, see Installation
+uv run deepaneseg-prepare source_dir=ADAM data_dir=Data 'labels=[1]' # or lay out your data as in Data
+uv run deepaneseg-remove-skull data_dir=Data
+uv run deepaneseg-extract-points data_dir=Data
+uv run deepaneseg-preprocess data_dir=Data
+uv run deepaneseg-train data_dir=Data name=exp1
+uv run deepaneseg-predict train_dir=Data/0Work/exp1
+uv run deepaneseg-evaluate train_dir=Data/0Work/exp1
+```
+Each step is described in [Usage](#usage), and its results in [Outputs](#outputs).
+
+## Installation
+The project uses [uv](https://docs.astral.sh/uv/) and Python 3.10+. From the repository folder, pick the PyTorch
+build:
+```
+uv sync --extra cu126 # NVIDIA GPU (CUDA 12.6)
+uv sync --extra cpu # CPU only, also the build used on macOS
+```
+This creates `.venv` with the exact versions of `uv.lock`. Commands then run with `uv run`, or directly once `.venv`
+is activated.
-# Background
-The major issue to address in medical image segmentation is the very severe class imbalance.
-To adress this problem, the most previous works were mainly focused on the network architecture and loss function. This paper tackles the data.
-Our data stratery is composed of the following three stages:
-* A rough but fast annotation is considered: each aneurysm is approximated by a sphere defined by two points.
-* A small patch approach is taken so as to increase the number of samples.
-* Samples are generated by a combination of **data selection** (negative patches are centered half on blood vessels and half on parenchyma) and **data synthesis** (patches containing an aneurysm are duplicated anddeformed by a 3D spline transform).
+Without uv, `pip install git+https://github.com/youssefassis/DeepAneSeg` installs the package and its commands
+(with PyTorch's default build from PyPI).
-# Project description
-The code is organised as a Python package, `deepaneseg`, plus command-line scripts:
+### Docker
+The image installs the same locked versions, with the GPU (default) or CPU PyTorch build:
```
-deepaneseg/
- data/ patient data I/O, train/valid/test splits, augmentation transforms
- volume/ patch extraction, sphere burning, point selection, skull stripping
- models/ 3D U-Net
- training/ dataset, losses, metrics, trainer
- inference/ patch-wise prediction and ADAM-style evaluation
- experimental/ post-paper models (vessel-coupled U-Nets) and their trainer
-configs/ Hydra configuration (see Configuration)
-scripts/ pipeline steps (see Usage)
- experimental/ analysis scripts from the PhD experiments
+docker build -t deepaneseg --build-arg GIT_COMMIT=$(git rev-parse HEAD) .
+docker build -t deepaneseg:cpu --build-arg TORCH=cpu .
```
+Mount the data folder and run any command; outputs are written to the mounted folder:
+```
+docker run --rm --gpus all --shm-size=8g -v /path/to/Data:/data deepaneseg \
+ deepaneseg-train data_dir=/data name=exp1
+```
+GPU runs need the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/) on
+the host. `--shm-size` gives the data-loading workers enough shared memory, and `--user $(id -u):$(id -g)` creates
+the output files with your own user.
-The data is expected in the following layout:
+## Data
+### Layout
+Each patient is a folder named `P` followed by four digits:
```
-Data_dir/
+Data/
P0001/
- volume.nii.gz 3D MRA image
- F.csv coordinates (x, y, z) of the two points defining each aneurysm
+ volume.nii.gz TOF-MRA scan
+ F.csv aneurysms: two points (x, y, z columns, in mm) on a diameter of each aneurysm
config.json {"init volume": "volume.nii.gz", "pts aneurysm": "F.csv"}
- noskull.nii.gz skull-stripped volume (created by remove_skull.py)
- points.csv negative patch centers (created by extract_points.py)
+ noskull.nii.gz skull-stripped scan (written by deepaneseg-remove-skull)
+ points.csv negative patch centers (written by deepaneseg-extract-points)
P0002/
...
- 0Work/ created by the scripts
- split_pats.json train/valid/test split
- / checkpoints, TensorBoard logs, predictions and resolved config (.hydra/) of one training
- logs/ logs of the preprocessing scripts
+ 0Work/ splits, trainings and logs (see Outputs)
```
+Patients without aneurysm simply have no `pts aneurysm` entry. Coordinates are in the scanner (RAS) space of the
+NIfTI file, so the points can be placed with any viewer, e.g. as markups in 3D Slicer.
-# Configuration
-Settings are [Hydra](https://hydra.cc) configs in `configs/`: one file per script (`train.yaml`, `predict.yaml`,
-`preprocess.yaml`, `remove_skull.yaml`, `extract_points.yaml`) composed from groups (`data`, `model`, `optimizer`,
-`scheduler`, `augmentation`). Any value can be overridden on the command line:
+### Converting a dataset
+`deepaneseg-prepare` converts a dataset of scans with aneurysm label masks to this layout. Its defaults match the
+ADAM challenge (`/orig/TOF.nii.gz` and `/aneurysms.nii.gz`, where 2 marks treated aneurysms); other
+datasets only need their own `scan` and `mask` patterns. Each aneurysm becomes a two-point annotation, and
+`Data/cases.csv` maps the patients to the original cases.
```
-uv run python scripts/train.py data_dir=Data_dir name=my_training batch_size=8 optimizer.lr=3e-4 model.f_maps=32
-uv run python scripts/train.py --cfg job --resolve data_dir=Data_dir name=my_training # print the config
+uv run deepaneseg-prepare source_dir=ADAM data_dir=Data 'labels=[1]'
+uv run deepaneseg-prepare source_dir=my_data data_dir=Data 'scan=scans/{case}.nii.gz' 'mask=masks/{case}.nii.gz'
```
-Runs are reproducible: `seed` (0 by default) fixes the patient split and the training, and each training writes
-`run_info.json` with the git commit and library versions next to its checkpoints. For identical GPU runs, also set
-`deterministic=true` (slower).
-
-To add an alternative (for example another scheduler), add a file to the group, such as
-`configs/scheduler/step.yaml` with a `_target_` class, and select it with `scheduler=step`.
-# Usage
-Run every command from the repository root.
-
-**1. Data preparation:** skull-strip the volumes, then select the negative patch centers.
+## Usage
+**1. Preprocessing:** skull-strip the scans, then select the negative patch centers.
```
-uv run python scripts/remove_skull.py data_dir=Data_dir
-uv run python scripts/extract_points.py data_dir=Data_dir
+uv run deepaneseg-remove-skull data_dir=Data
+uv run deepaneseg-extract-points data_dir=Data
```
-**2. Split:** assign the patients to training, validation and testing.
+**2. Split:** assign the patients to training, validation and testing (70/20/10 by default).
```
-uv run python scripts/preprocess_data.py data_dir=Data_dir # split=[0.7,0.2,0.1] overwrite=true
+uv run deepaneseg-preprocess data_dir=Data # 'split=[0.8,0.1,0.1]' overwrite=true
```
-**3. Training:** outputs go to `Data_dir/0Work/my_training`; running the same command again resumes training.
-The training and validation volumes are kept in memory as float32, about 4 bytes per voxel (150 MiB for a
-512×512×150 scan).
+**3. Training:** outputs go to `Data/0Work/exp1`; running the same command again resumes the training.
```
-uv run python scripts/train.py data_dir=Data_dir name=my_training
+uv run deepaneseg-train data_dir=Data name=exp1
```
-**4. Testing:** predictions of the test patients are written to `Data_dir/0Work/my_training/prediction/test`: a
-probability map per patient, and its detections (center and radius in mm, size, probability) as CSV and as `.fcsv`
-markups for 3D Slicer.
+**4. Prediction:** writes a probability map and the detections of each test patient.
+```
+uv run deepaneseg-predict train_dir=Data/0Work/exp1
+```
+Any other scan, or folder of scans, can be predicted the same way. Scans are skull-stripped first, like the
+training data.
```
-uv run python scripts/predict.py train_dir=Data_dir/0Work/my_training
+uv run deepaneseg-predict train_dir=Data/0Work/exp1 input=scan.nii.gz output_dir=results
```
-Any other scan, or folder of scans, can be predicted the same way (skull-stripped first, like the training data):
+
+**5. Evaluation:** detection metrics of the test predictions, with the ADAM challenge criteria (a detection counts
+when its center lies within an aneurysm): sensitivity and false positives per patient.
```
-uv run python scripts/predict.py train_dir=Data_dir/0Work/my_training input=scan.nii.gz output_dir=results
+uv run deepaneseg-evaluate train_dir=Data/0Work/exp1 # threshold=0.5 min_size=null
```
-**5. Evaluation:** detection metrics of the test predictions (ADAM challenge criteria: sensitivity and false
-positives per patient), written to `Data_dir/0Work/my_training/evaluation`.
+## Configuration
+Settings are [Hydra](https://hydra.cc) configs in `deepaneseg/configs/`: one file per command, composed from groups
+(`data`, `model`, `optimizer`, `scheduler`, `augmentation`). Any value can be overridden on the command line:
```
-uv run python scripts/evaluate.py train_dir=Data_dir/0Work/my_training # threshold=0.5 min_size=null
+uv run deepaneseg-train data_dir=Data name=exp2 batch_size=8 optimizer.lr=3e-4 model.f_maps=32
+uv run deepaneseg-train --cfg job --resolve data_dir=Data name=exp2 # print the resulting config
```
+To add an alternative, e.g. another scheduler, add a file with a `_target_` class to the group, such as
+`deepaneseg/configs/scheduler/step.yaml`, and select it with `scheduler=step`.
-# Differences from the published code
-The Kappa metric/loss follows Cohen's definition and the focal loss is computed per voxel (Lin et al.), so
-trainings using them can give numbers that differ slightly from the published ones.
+Runs are reproducible: `seed` (0 by default) fixes the split and the training, and each training records its git
+commit and library versions in `run_info.json`. For identical GPU runs, also set `deterministic=true` (slower).
-# Installation
-The project uses [uv](https://docs.astral.sh/uv/) (Python 3.10+). From the repository root, pick the PyTorch build:
+## Outputs
+Everything is written under `Data/0Work`:
```
-uv sync --extra cu126 # NVIDIA GPU (CUDA 12.6)
-uv sync --extra cpu # CPU only (also the build used on macOS)
+split_pats.json train/valid/test split
+logs/ logs of the preprocessing commands
+exp1/ one training
+ .hydra/config.yaml resolved configuration, read back by deepaneseg-predict and deepaneseg-evaluate
+ run_info.json git commit and library versions
+ last_checkpoint.pytorch model after the last epoch, used for prediction by default
+ best_checkpoint.pytorch model with the best validation metric
+ logs/ TensorBoard logs: uv run tensorboard --logdir Data/0Work
+ train.log
+ prediction/test/ for each test patient (prediction/scans/ for input=...):
+ .nii.gz probability map, on the patient's voxel grid
+ _detections.csv one row per detection: x, y, z and radius (mm), voxels, probability
+ _detections.fcsv the same detections as 3D Slicer markups
+ evaluation/ per_patient.csv and summary.json
```
-This creates `.venv` with the exact versions of `uv.lock`, including pytest and ruff. Commands then run with
-`uv run`, e.g. `uv run pytest`. Without uv, `pip install -e .` also works (PyTorch's default PyPI build).
-# Docker
-Build the image with the GPU (default) or CPU PyTorch build:
+## Requirements
+- **Memory:** training keeps the training and validation scans in memory, about 4 bytes per voxel: 150 MiB for a
+ 512×512×150 scan, around 16 GiB for 111 patients.
+- **GPU:** the U-Net has 19.1 M parameters (73 MiB). Its memory use comes mostly from the batch of 20 patches of
+ 48³ voxels; if the GPU runs out of memory, lower `batch_size` and `validation_batch_size`.
+- **Disk:** predictions are float32 probability maps, one per scan.
+
+## Development
```
-docker build -t deepaneseg --build-arg GIT_COMMIT=$(git rev-parse HEAD) .
-docker build -t deepaneseg:cpu --build-arg TORCH=cpu .
+uv run pytest # tests
+uv run ruff check . # lint
+uv run ruff format . # formatting
```
-Mount the data folder and run any script; outputs are written under `Data_dir/0Work` on the host:
+The code is organised as follows:
```
-docker run --rm --gpus all --shm-size=8g -v /path/to/Data_dir:/data deepaneseg \
- python scripts/train.py data_dir=/data name=my_training
-docker run --rm --gpus all --shm-size=8g -v /path/to/Data_dir:/data deepaneseg \
- python scripts/predict.py train_dir=/data/0Work/my_training
+deepaneseg/
+ cli/ commands
+ configs/ Hydra configuration
+ data/ patient data I/O, dataset conversion, splits, augmentation transforms
+ volume/ patch extraction, sphere burning, point selection, skull stripping
+ models/ 3D U-Net
+ training/ dataset, losses, metrics, trainer
+ inference/ patch-wise prediction, detections and evaluation
+ experimental/ post-paper models (vessel-coupled U-Nets) and their trainer
+scripts/experimental/ analysis scripts from the PhD experiments
```
-GPU runs need the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/) on the
-host. `--shm-size` gives the data-loading workers enough shared memory, and `--user $(id -u):$(id -g)` creates the
-output files with your own user.
-# Citation
-If you find this repository useful in your research, please consider citing the paper below. The same reference
-is available from "Cite this repository" on GitHub (`CITATION.cff`).
+## Differences from the published code
+The Kappa metric/loss follows Cohen's definition and the focal loss is computed per voxel (Lin et al.), so trainings
+using them can give numbers that differ slightly from the published ones.
+
+## Citation
+If you find this work useful, please cite the paper (also available from "Cite this repository" on GitHub):
```
@inproceedings{assis2021efficient,
title={An efficient data strategy for the detection of brain aneurysms from MRA with deep learning},
@@ -144,5 +217,5 @@ is available from "Cite this repository" on GitHub (`CITATION.cff`).
}
```
-# Acknowledgements
-This work was funded by the Grand-Est Region, and the University Hospital (CHRU) of Nancy, France
+## Acknowledgements
+This work was funded by the Grand-Est Region and the University Hospital (CHRU) of Nancy, France.
diff --git a/deepaneseg/cli/__init__.py b/deepaneseg/cli/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scripts/evaluate.py b/deepaneseg/cli/evaluate.py
similarity index 94%
rename from scripts/evaluate.py
rename to deepaneseg/cli/evaluate.py
index 1168e42..d845995 100644
--- a/scripts/evaluate.py
+++ b/deepaneseg/cli/evaluate.py
@@ -1,7 +1,6 @@
-#!/usr/bin/env python3
-"""Score a training's predictions on its test patients; configuration in configs/evaluate.yaml.
+"""Score a training's predictions on its test patients; configuration in deepaneseg/configs/evaluate.yaml.
-Example: python scripts/evaluate.py train_dir=/data/0Work/exp1
+Example: deepaneseg-evaluate train_dir=/data/0Work/exp1
A detection (connected component above the threshold) is a true positive when its center lies within an
aneurysm's radius (ADAM challenge criteria). Writes per_patient.csv and summary.json to /evaluation.
"""
diff --git a/scripts/extract_points.py b/deepaneseg/cli/extract_points.py
similarity index 81%
rename from scripts/extract_points.py
rename to deepaneseg/cli/extract_points.py
index 571e5ba..b08eef4 100644
--- a/scripts/extract_points.py
+++ b/deepaneseg/cli/extract_points.py
@@ -1,7 +1,6 @@
-#!/usr/bin/env python3
"""Select negative patch centers (on vessels and in the parenchyma) for every patient.
-Configuration in configs/extract_points.yaml. Example: python scripts/extract_points.py data_dir=/data
+Configuration in deepaneseg/configs/extract_points.yaml. Example: deepaneseg-extract-points data_dir=/data
Writes